Repository navigation
[Feat/#115] 행사 신청내역 화면 구현 (목록·빈 상태·상세·신청취소) - #116
sangrae2325 wants to merge 20 commits into
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| setToastCount((count) => count + 1); | ||
| }; | ||
|
|
||
| const cancelApplication = async () => { |
There was a problem hiding this comment.
[c] 신청 취소 요청, try/catch, isCancelling 상태, 토스트 상태와 key, startTransition까지 취소 로직이 화면 파일에 다 들어 있어서, 훅으로 빼면 화면이 헤더·모달·토스트를 그리는 일만 맡을 수 있을 것 같습니다. 사물함의 useLockersApplySubmit과 같은 방식으로요!
// features/events/hooks/useEventsApplicationCancel.ts
export function useEventsApplicationCancel(applicationId: string) {
const [isCancelling, setIsCancelling] = useState(false);
const [toast, setToast] = useState<CancelToast | null>(null);
// ...
const cancel = async () => { /* 지금의 cancelApplication */ };
return { isCancelling, cancel, toast, clearToast };
}토스트 문구와 색도 JSX 안에서 toast === "failed" 삼항 연산자로 두 번 고르고 있는데(102·112번째 줄), 상수 하나로 묶으면 읽기 편할 것 같습니다.
const CANCEL_TOASTS = {
cancelled: { message: "신청이 취소됐어요.", variant: "positive" },
failed: { message: "신청 취소에 실패했어요. 다시 시도해 주세요.", variant: "negative" },
} as const;|
|
||
| // 신청 취소처럼 목데이터를 바꾸는 요청에 쓴다. 조회와 같은 지연을 주지만 캐시하지 않는다. | ||
| // 바뀐 데이터를 다시 받게 하려면 응답 뒤 관련 조회 key를 invalidateMockupApi로 지운다. | ||
| export async function mockupMutation<T>(mutate: () => T): Promise<T> { |
There was a problem hiding this comment.
[c] PR 리뷰 포인트 [c]에 대한 답변입니다.
나중에 TanStack Query의 mutation + invalidateQueries로 바로 옮겨갈 수 있는 모양이라 방향은 좋은 것 같습니다! 다른 화면에서도 쓰려면 두 가지만 정리해두면 좋을 것 같아요.
① 캐시 key를 조회와 무효화가 따로 만듦: eventsApi.ts에서 조회는 34·42번째 줄, 무효화는 70~72번째 줄에서 각자 문자열을 이어 붙이고 있어서, 나중에 조회 쪽 key를 바꾸면 무효화가 조용히 빗나갈 수 있을 것 같습니다. key 함수를 하나 두고 양쪽이 같이 쓰면 어떨까요?
const eventsKeys = {
applications: () => "events/applications",
application: (id: string) => `events/applications/${id}`,
};② 캐시를 지우면 다시 그릴 때 스켈레톤이 뜸: 그래서 호출하는 화면마다 startTransition으로 감싸야 하고, 화면이 "API가 캐시를 지운다"는 내부 사정까지 알아야 하는 것 같아요(EventsApplicationDetailScreen.tsx:40 주석). 팀 패턴으로 쓴다면 coding-style.md "데이터 로딩" 절에 "바꾸는 요청은 mockupMutation → invalidateMockupApi, 다시 그리는 업데이트는 transition으로" 같은 규칙을 적어두면, 다음에 쓰는 사람이 같은 고민을 반복하지 않을 것 같습니다.
| const navigate = useNavigate(); | ||
| const [tab, setTab] = useState("event"); | ||
| const { pathname } = useLocation(); | ||
| const tab: EventsTab = |
There was a problem hiding this comment.
[c] 탭을 pathname === EVENTS_TAB_PATHS.application처럼 정확히 같은지 비교해서 정하는데, react-router는 끝에 /가 붙은 /events/applications/도 같은 라우트로 매칭해서 그 주소로 들어오면 탭이 "행사"로 잡힐 것 같습니다. ScreenLayout.tsx:37도 같은 비교라 하단 탭은 "홈"이 되고요.
라우터에서 탭을 prop으로 넘기면 화면이 주소를 해석할 필요가 없어질 것 같은데 어떨까요?
// router.tsx
{ element: <EventsListScreen tab="event" />, path: "/events" },
{
element: <EventsListScreen tab="application" />,
handle: { bottomNavValue: "event" } satisfies ScreenRouteHandle, // 필드 이름은 예시
path: "/events/applications",
},하단 탭 소속도 coding-style.md의 "새 옵션은 ScreenRouteHandle에 필드를 추가한다" 규칙대로 handle에 적으면, 주석에 적어주신 "공용 UI에 기능 경로를 그대로 적는" 중복도 없어질 것 같습니다.
| } as const; | ||
|
|
||
| // "2026-06-04T13:00" → "2026.06.04 13:00" | ||
| export function formatApplicationDateTime(value: string) { |
There was a problem hiding this comment.
[c] formatApplicationDateTime이 #113의 FeePaymentCard에 있는 formatDateTime과 같은 코드(replace("T", " ").replaceAll("-", "."))인 것 같습니다. eventsApi.ts:26의 toLocalDateTime까지 날짜 함수가 PR마다 따로 생기고 있어서, #113에 남긴 lib/date.ts 의견처럼 한곳에 모으면 좋을 것 같아요. 두 PR 중 먼저 머지되는 쪽에서 만들고 나중 쪽이 가져다 쓰면 될 것 같습니다!
| application: MyEventApplication; | ||
| }) { | ||
| const badge = EVENTS_APPLICATION_STATUS_BADGES[application.status]; | ||
| const rows = [ |
There was a problem hiding this comment.
[a] "배지 + 제목 + 라벨/값 행" 카드가 #113의 FeePaymentCard·LockersAssignmentContent와 마크업이 거의 같은 것 같습니다(heading2 제목, label2 라벨/값, gap-1.5). 컴포넌트 컨벤션의 "두 번째 화면에서 재사용되면 components/ui/로 옮긴다" 기준을 넘은 것 같은데, PR이 서로 달라서 지금 바꾸기보다는 이슈로 묶어두면 좋을 것 같아요.
| onAnswerChange, | ||
| disabled = false, | ||
| }: EventsQuestionFieldProps) { | ||
| const isReadOnly = onAnswerChange === undefined; |
There was a problem hiding this comment.
[a] 읽기 전용 여부를 onAnswerChange를 넘겼는지로 정하고 있어서, 신청 폼에서 실수로 콜백을 빠뜨려도 에러 없이 읽기 전용 폼이 될 것 같습니다(211번째 줄의 RadioGroup도 undefined가 그대로 넘어가요). disabled는 이미 명시적인 prop이니 읽기 전용도 prop으로 받으면 의도가 더 잘 드러날 것 같아요.
<EventsQuestionField readOnly disabled={isCancelled} answer={...} question={...} />#113의 LockersCell(onSelect가 없으면 보기 전용)에도 같은 의견을 남겼습니다!
There was a problem hiding this comment.
readOnly prop을 받게 바꾸고, 입력 폼은 onAnswerChange가 필수가 되도록 해서 콜백을 빠뜨리면 에러가 나도록 했습니다! (8b5daab)
| description: string; | ||
| /** 있을 때만 버튼을 노출한다 — Figma는 모집중 필터에만 "아카이빙 둘러보기"를 둔다 */ | ||
| actionLabel?: string; | ||
| onAction?: () => void; |
There was a problem hiding this comment.
[a] actionLabel과 onAction을 따로 받아서, 문구만 있고 동작이 없으면 반응 없는 버튼이 그려지는 것 같습니다(지금 "아카이빙 둘러보기"가 그 상태예요). ResultScreen의 primaryAction처럼 하나로 묶으면 그런 조합을 타입에서 막을 수 있을 것 같습니다.
action?: { label: string; onClick: () => void };그리고 main에 아카이빙(#40, /archives)이 이미 들어와 있어서, main을 합치면 55번째 줄의 TODO도 바로 연결할 수 있을 것 같아요!
|
|
||
| // 없는 신청내역이면 볼 내용이 없어서 신청내역 목록으로 돌려보낸다 | ||
| if (!application) { | ||
| return <Navigate replace to={EVENTS_TAB_PATHS.application} />; |
There was a problem hiding this comment.
[a] 이전 PR들(#97, #113)에 남긴 의견과 같은 내용이라 짧게 남깁니다!
- 데이터 컴포넌트 안에서
<Navigate>로 직접 이동하고 있는데, 이동은 화면이 콜백이나 경로 prop으로 넘겨주는 규칙이라 그쪽으로 맞추면 좋을 것 같습니다. 경로 상수를 써주셔서 #113보다는 훨씬 나은 것 같아요. EventsApplicationDetailScreen.tsx:52의goBack대비 로직도 같은 코드가 하나 더 복사돼서,useBackNavigation(fallbackPath)같은 훅이 생기면 그걸로 바꾸면 될 것 같습니다.
| disabled?: boolean; | ||
| } | ||
|
|
||
| const TEXT_AREA_TYPOGRAPHY = css` |
There was a problem hiding this comment.
[a] TEXT_AREA_TYPOGRAPHY가 WDS TextArea 안쪽의 textarea(자동 높이를 재는 숨은 textarea 포함)를 선택자로 직접 잡고 있어서, WDS를 업데이트해 내부 구조가 바뀌면 조용히 깨질 수 있을 것 같습니다. wds-component-usage.md에 적어주셔서 추적은 될 것 같은데, 기존 신청 폼 글자도 16→14px로 바뀌니 디자이너 확인을 한번 받아두면 좋을 것 같아요.
There was a problem hiding this comment.
WDS 내부 클래스가 아니라 textarea태그를 고르는 방식으로 수정했습니다. Figma에서도 신청 폼 글씨 크기가 14px 입니다!!
| onClick={onCancel} | ||
| size="medium" | ||
| sx={{ | ||
| boxShadow: "inset 0 0 0 1px var(--color-status-negative)", |
There was a problem hiding this comment.
[a] 빨간(negative) 버튼을 만드는 방식이 세 가지가 된 것 같습니다. ConfirmModal은 sx로 배경을, #97의 탈퇴 버튼도 sx로 배경을 덮고, 여기는 boxShadow + color를 덮어요. negative 버튼 스타일을 한곳(상수나 작은 래퍼 컴포넌트)에 모아두면, 나중에 색이나 테두리가 바뀔 때 한 번에 맞출 수 있을 것 같습니다.
There was a problem hiding this comment.
negativeButtonSx.ts로 모았고 ConfirmModal과 신청 취소 버튼이 같이 쓰도록 했습니다. (8cd965a)
…ications # Conflicts: # docs/conventions/wds-component-usage.md # src/features/events/EventsListScreen.tsx
|
남겨주신 코멘트 모두 반영했습니다!! 따로 5건에 답글 남겨두었습니다. |
…to feat/#115-events-applications
#️⃣연관된 이슈
🎯 해결하려는 문제가 무엇인가요?
행사 목록의
신청내역탭이 "신청내역은 아직 준비 중이에요" 문구로만 남아 있었고, 행사 신청 완료 화면의신청내역 보기는 없는 주소(/my/applications)로 가서 준비 중 화면이 떴다. Figma 행사 신청내역 섹션(1133:44426)을 목데이터로 구현한다./events/applications1133:46168/events/applications1165:63551/events/applications/:applicationId1133:461811133:46131/events/applications/:applicationId1133:46216❓ 왜 해결해야 하나요?
신청한 행사를 확인하거나 취소할 곳이 없다. 홈의 신청한 행사
더보기·행(#114)도 이 화면으로 연결해야 한다.실제뷰
⭐ 어떻게 해결했나요?
1. 데이터 (main 규칙:
entities+mockupApi+use()+ 스켈레톤)entities/events에MyEventApplication타입·목데이터·API 추가fetchMyEventApplications(),fetchMyEventApplication(id),cancelMyEventApplication(id)EventsQuestion,EventsAnswer등)을features/events/constants에서entities/events/types.ts로 옮겼다. 신청 폼은 import 경로만 바뀐다lib/mockupApi.ts에 바꾸는 요청용mockupMutation, 캐시를 지우는invalidateMockupApi추가2. 행사 목록 탭을 경로로 나눔
/events, 신청내역/events/applications. 같은 목록 화면이 주소로 탭을 고른다replace라 히스토리가 쌓이지 않고, 새로고침해도 탭이 유지된다/events/applications에서도 "행사"가 선택된다신청내역 보기→/events/applications3. 신청내역 목록·빈 상태
행사 상세·신청 상세EventsEmptyState를 재사용하고행사 둘러보기→ 행사 탭4. 신청내역 상세
EventsQuestionField로 읽기 전용 표시Label/Disable글자로 흐리게5. 신청 취소
신청 취소→ConfirmModal tone="negative"→ 취소 → 상세가 취소 상태로 바뀌고 "신청이 취소됐어요." 토스트6. 공용 컴포넌트 수정
EventsQuestionField: 읽기 전용 모드(변경 콜백을 안 넘기면)·disabled추가, 읽기 전용이면aria-readonlyEventsQuestionField: WDSTextArea글자가 16px로 고정돼 Figma(Label 1/Reading14px)와 달라서sx로 덮었다. 기존 신청 폼 입력칸도 14px로 바뀐다EventsEmptyState: 버튼 동작(onAction)·설명 굵기 옵션 추가, px 임의값을 Tailwind 크기로 정리7. 문서 —
wds-component-usage.md에 행사 신청내역 절 추가, 신청 폼 TextArea 항목에 글자 덮어쓰기 추가🧩 이 PR의 한계 & 트레이드오프
신청 취소버튼이 있어서, 행사가 끝났거나 취소 기한이 지난 경우도 지금은 버튼이 보인다신청내역 보기(→/my/locker)는 #114에서 한다⛓️ 기존 기능에 미치는 영향
신청내역 보기가 준비 중 화면 대신 신청내역 탭으로 간다🔀 Edge Case & 실패 시나리오
📋 검토한 대안과 선택 이유
?tab=)·navigate state vs 경로 — 주소가 깔끔하면서 새로고침·다른 화면에서의 진입에도 탭이 유지되는 경로로 했다EventsSummaryCard재사용 vs 새로 — 기존 카드는 아이콘 + 일시·장소라 Figma의 "행사·신청·취소" 라벨 행과 달라서 새로 만들었다💬 리뷰 포인트
[c]lib/mockupApi.ts의mockupMutation·invalidateMockupApi— 다른 화면의 "바꾸는 요청"에도 이 방식을 쓸지[a]취소할 수 없는 경우(행사 종료·기한 경과) 처리는 디자인 확정 후