Skip to content

[Feat/#115] 행사 신청내역 화면 구현 (목록·빈 상태·상세·신청취소) - #116

Open
sangrae2325 wants to merge 20 commits into
feat/#111-my-history-screensfrom
feat/#115-events-applications
Open

sangrae2325 wants to merge 20 commits into
feat/#111-my-history-screensfrom
feat/#115-events-applications

Conversation

@sangrae2325

@sangrae2325 sangrae2325 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

📚 스택 PR입니다 — base가 #113(feat/#111-my-history-screens) 브랜치예요. Files changed에는 이 PR 변경만 보입니다.
리뷰 반영 때 #113과 같은 공용 파일(lib/date, useBackNavigation, icons/common/)을 쓰게 돼서 #113 위로 쌓았습니다(c332556).
머지 순서: #113 → #116 → #117. #113이 머지되면 base가 자동으로 main으로 바뀌고, 그때 main을 한 번 merge해 정리한 뒤 머지합니다.

🎯 해결하려는 문제가 무엇인가요?

행사 목록의 신청내역 탭이 "신청내역은 아직 준비 중이에요" 문구로만 남아 있었고, 행사 신청 완료 화면의 신청내역 보기는 없는 주소(/my/applications)로 가서 준비 중 화면이 떴다. Figma 행사 신청내역 섹션(1133:44426)을 목데이터로 구현한다.

화면 주소 Figma
신청내역 목록 /events/applications 1133:46168
신청내역 빈 상태 /events/applications 1165:63551
신청내역 상세 /events/applications/:applicationId 1133:46181
신청취소 확인모달 (상세 위) 1133:46131
신청취소 상세 + 토스트 /events/applications/:applicationId 1133:46216

❓ 왜 해결해야 하나요?

신청한 행사를 확인하거나 취소할 곳이 없다. 홈의 신청한 행사 더보기·행(#114)도 이 화면으로 연결해야 한다.

실제뷰

image image image image

⭐ 어떻게 해결했나요?

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/applications

3. 신청내역 목록·빈 상태

  • 카드: 회색 썸네일 + 신청완료(초록)/신청취소(빨강) 배지 + 행사명 + 신청/취소 일시 + 행사 상세·신청 상세
  • 신청내역 탭에서는 Figma대로 상태 필터 행을 숨긴다
  • 빈 상태는 EventsEmptyState를 재사용하고 행사 둘러보기 → 행사 탭

4. 신청내역 상세

  • 행사 요약(행사·신청·취소 일시) + 내가 낸 답변을 신청 폼의 EventsQuestionField로 읽기 전용 표시
  • 신청취소 상세는 입력들을 WDS 비활성 모양 + Label/Disable 글자로 흐리게

5. 신청 취소

  • 신청 취소 → ConfirmModal tone="negative" → 취소 → 상세가 취소 상태로 바뀌고 "신청이 취소됐어요." 토스트
  • 취소 API가 목데이터를 바꾸고 목록·상세 조회 캐시를 지워서, 목록으로 돌아가도 취소 상태로 보인다
  • 다시 받는 동안 스켈레톤이 깜빡이지 않도록 다시 그리는 업데이트를 transition으로 묶었다

6. 공용 컴포넌트 수정

  • EventsQuestionField: 읽기 전용 모드(변경 콜백을 안 넘기면)·disabled 추가, 읽기 전용이면 aria-readonly
  • EventsQuestionField: WDS TextArea 글자가 16px로 고정돼 Figma(Label 1/Reading 14px)와 달라서 sx로 덮었다. 기존 신청 폼 입력칸도 14px로 바뀐다
  • EventsEmptyState: 버튼 동작(onAction)·설명 굵기 옵션 추가, px 임의값을 Tailwind 크기로 정리

7. 문서 — wds-component-usage.md에 행사 신청내역 절 추가, 신청 폼 TextArea 항목에 글자 덮어쓰기 추가

🧩 이 PR의 한계 & 트레이드오프

  • 목데이터라 새로고침하면 취소가 풀린다. 취소 일시도 누른 시각으로 찍힌다
  • 취소할 수 없는 경우가 정의돼 있지 않다. Figma는 신청완료면 항상 신청 취소 버튼이 있어서, 행사가 끝났거나 취소 기한이 지난 경우도 지금은 버튼이 보인다
  • 실패 토스트 문구("신청 취소에 실패했어요. 다시 시도해 주세요.")는 Figma에 없어서 임의로 정했다
  • 썸네일은 행사 이미지 API가 없어서 회색 자리다
  • 홈 신청한 행사 연결, 사물함 신청 완료 신청내역 보기(→ /my/locker)는 #114에서 한다

⛓️ 기존 기능에 미치는 영향

  • 행사 신청 폼: 서술형 입력칸 글자가 16px → 14px(Figma 값)로 바뀐다. 자동 높이는 그대로 맞는다(빈 칸 22px, 세 줄 66px). 선택·입력 동작은 그대로다
  • 행사 목록: 탭이 주소로 바뀌었다. 행사 탭에서 고른 필터는 신청내역 탭에 다녀와도 유지된다
  • 행사 목록 빈 상태: 그림을 Figma보다 2px 치우친 위치 대신 가운데에 둔다(px 정리)
  • 행사 신청 완료: 신청내역 보기가 준비 중 화면 대신 신청내역 탭으로 간다

🔀 Edge Case & 실패 시나리오

  • 없는 신청내역 id로 상세에 들어오면 신청내역 목록으로 보낸다
  • 주소로 바로 들어와 뒤로 갈 기록이 없으면 신청내역 목록으로 보낸다
  • 취소 요청 중에는 버튼을 막아 두 번 보내지 않는다. 실패하면 실패 토스트를 띄우고 화면은 그대로 둔다
  • 읽기 전용 상세에서 답하지 않은 서술형 문항은 입력 안내 문구 없이 빈 칸으로 둔다

📋 검토한 대안과 선택 이유

  • 탭: 쿼리 파라미터(?tab=)·navigate state vs 경로 — 주소가 깔끔하면서 새로고침·다른 화면에서의 진입에도 탭이 유지되는 경로로 했다
  • 상세 문항 데이터: features 상수 import vs 신청내역이 문항을 같이 보관 — 행사 문항이 바뀌어도 내가 답한 그대로 보여야 해서 신청 당시 문항을 같이 보관한다
  • 취소 후 목록: 상세 화면 안에서만 반영 vs 목데이터·캐시까지 반영 — 실제 API와 같은 흐름을 미리 맞추려고 목록까지 반영했다
  • 상세 요약 카드: EventsSummaryCard 재사용 vs 새로 — 기존 카드는 아이콘 + 일시·장소라 Figma의 "행사·신청·취소" 라벨 행과 달라서 새로 만들었다

💬 리뷰 포인트

  • [c] lib/mockupApi.ts의 mockupMutation·invalidateMockupApi — 다른 화면의 "바꾸는 요청"에도 이 방식을 쓸지
  • [a] 취소할 수 없는 경우(행사 종료·기한 경과) 처리는 디자인 확정 후

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3c997c0e-8186-47a1-b753-aad17ad83424
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sangrae2325
sangrae2325 marked this pull request as ready for review October 10, 2026 14:02
setToastCount((count) => count + 1);
};

const cancelApplication = async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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;

Comment thread src/lib/mockupApi.ts

// 신청 취소처럼 목데이터를 바꾸는 요청에 쓴다. 조회와 같은 지연을 주지만 캐시하지 않는다.
// 바뀐 데이터를 다시 받게 하려면 응답 뒤 관련 조회 key를 invalidateMockupApi로 지운다.
export async function mockupMutation<T>(mutate: () => T): Promise<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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에 기능 경로를 그대로 적는" 중복도 없어질 것 같습니다.

Comment thread src/features/events/constants/events.ts Outdated
} as const;

// "2026-06-04T13:00" → "2026.06.04 13:00"
export function formatApplicationDateTime(value: string) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] formatApplicationDateTime이 #113의 FeePaymentCard에 있는 formatDateTime과 같은 코드(replace("T", " ").replaceAll("-", "."))인 것 같습니다. eventsApi.ts:26의 toLocalDateTime까지 날짜 함수가 PR마다 따로 생기고 있어서, #113에 남긴 lib/date.ts 의견처럼 한곳에 모으면 좋을 것 같아요. 두 PR 중 먼저 머지되는 쪽에서 만들고 나중 쪽이 가져다 쓰면 될 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#113 과 같은 lib/date.ts로 수정했습니다.(298fceb) toLocalDateTime은 화면 표기용이 아니라 목데이터의 취소 시각을 만드는 함수라, eventsMock.ts로 옮기고 API 연동 때 같이 지우도록 주석 남겼습니다!

application: MyEventApplication;
}) {
const badge = EVENTS_APPLICATION_STATUS_BADGES[application.status];
const rows = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] "배지 + 제목 + 라벨/값 행" 카드가 #113의 FeePaymentCard·LockersAssignmentContent와 마크업이 거의 같은 것 같습니다(heading2 제목, label2 라벨/값, gap-1.5). 컴포넌트 컨벤션의 "두 번째 화면에서 재사용되면 components/ui/로 옮긴다" 기준을 넘은 것 같은데, PR이 서로 달라서 지금 바꾸기보다는 이슈로 묶어두면 좋을 것 같아요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신대로 #120 으로 이슈를 따로 팠습니다! #113 , #116 이 머지된 뒤 한 번에 맞추겠습니다!

onAnswerChange,
disabled = false,
}: EventsQuestionFieldProps) {
const isReadOnly = onAnswerChange === undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 읽기 전용 여부를 onAnswerChange를 넘겼는지로 정하고 있어서, 신청 폼에서 실수로 콜백을 빠뜨려도 에러 없이 읽기 전용 폼이 될 것 같습니다(211번째 줄의 RadioGroup도 undefined가 그대로 넘어가요). disabled는 이미 명시적인 prop이니 읽기 전용도 prop으로 받으면 의도가 더 잘 드러날 것 같아요.

<EventsQuestionField readOnly disabled={isCancelled} answer={...} question={...} />

#113의 LockersCell(onSelect가 없으면 보기 전용)에도 같은 의견을 남겼습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readOnly prop을 받게 바꾸고, 입력 폼은 onAnswerChange가 필수가 되도록 해서 콜백을 빠뜨리면 에러가 나도록 했습니다! (8b5daab)

description: string;
/** 있을 때만 버튼을 노출한다 — Figma는 모집중 필터에만 "아카이빙 둘러보기"를 둔다 */
actionLabel?: string;
onAction?: () => void;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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} />;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 이전 PR들(#97, #113)에 남긴 의견과 같은 내용이라 짧게 남깁니다!

  • 데이터 컴포넌트 안에서 <Navigate>로 직접 이동하고 있는데, 이동은 화면이 콜백이나 경로 prop으로 넘겨주는 규칙이라 그쪽으로 맞추면 좋을 것 같습니다. 경로 상수를 써주셔서 #113보다는 훨씬 나은 것 같아요.
  • EventsApplicationDetailScreen.tsx:52의 goBack 대비 로직도 같은 코드가 하나 더 복사돼서, useBackNavigation(fallbackPath) 같은 훅이 생기면 그걸로 바꾸면 될 것 같습니다.

disabled?: boolean;
}

const TEXT_AREA_TYPOGRAPHY = css`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] TEXT_AREA_TYPOGRAPHY가 WDS TextArea 안쪽의 textarea(자동 높이를 재는 숨은 textarea 포함)를 선택자로 직접 잡고 있어서, WDS를 업데이트해 내부 구조가 바뀌면 조용히 깨질 수 있을 것 같습니다. wds-component-usage.md에 적어주셔서 추적은 될 것 같은데, 기존 신청 폼 글자도 16→14px로 바뀌니 디자이너 확인을 한번 받아두면 좋을 것 같아요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDS 내부 클래스가 아니라 textarea태그를 고르는 방식으로 수정했습니다. Figma에서도 신청 폼 글씨 크기가 14px 입니다!!

onClick={onCancel}
size="medium"
sx={{
boxShadow: "inset 0 0 0 1px var(--color-status-negative)",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 빨간(negative) 버튼을 만드는 방식이 세 가지가 된 것 같습니다. ConfirmModal은 sx로 배경을, #97의 탈퇴 버튼도 sx로 배경을 덮고, 여기는 boxShadow + color를 덮어요. negative 버튼 스타일을 한곳(상수나 작은 래퍼 컴포넌트)에 모아두면, 나중에 색이나 테두리가 바뀔 때 한 번에 맞출 수 있을 것 같습니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

negativeButtonSx.ts로 모았고 ConfirmModal과 신청 취소 버튼이 같이 쓰도록 했습니다. (8cd965a)

@sangrae2325

Copy link
Copy Markdown
Collaborator Author

남겨주신 코멘트 모두 반영했습니다!! 따로 5건에 답글 남겨두었습니다.

@sangrae2325
sangrae2325 changed the base branch from main to feat/#111-my-history-screens October 15, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

행사 신청내역 화면 구현 (목록·빈 상태·상세·신청취소)

2 participants