diff --git a/docs/conventions/wds-component-usage.md b/docs/conventions/wds-component-usage.md index 3251213..4f25c9a 100644 --- a/docs/conventions/wds-component-usage.md +++ b/docs/conventions/wds-component-usage.md @@ -584,3 +584,43 @@ A-1 본문의 `Zone Grid Row`(`2159:110823`)가 `h-px`라 `위쪽 칸/아래쪽 - **재사용(수정 없음)**: 행사 `EventsCard`, 공지 `NoticesCard`, 열린피드백 `FeedbacksCard` - **빌릴게**: `BililgeItemCard`에 `bordered` prop만 추가했다(흰 배경 위에서 테두리 `border-line-solid-neutral`). 메인 목록은 회색 배경 위라 테두리가 없다 - **아카이빙**: Figma `Search Archiving Card`(`3013:127633`)는 Stream 로컬이고 이 화면 전용이라 `features/search/components/SearchArchivingCard.tsx`로 새로 만들었다 + +## 학생회비 계좌 송금 플로우(`3562:162833` 외 5종) 구현 중 확정된 매핑 + +| Figma 인스턴스 | 코드 | 확정 근거 | +|---|---|---| +| `Textinput/Textfield` | `TextField`(`@wanteddev/wds`) | 메인 컴포넌트 Node ID `445:8591` + [공식 문서](https://montage.wanted.co.kr/docs/components/selection-and-input/text-field/design). 남은 학기 수 입력칸(`3562:162844`)이 335×48·radius 12px로 실측 일치 | +| `Icon/Normal/Upload` | `IconUpload`(`@wanteddev/wds-icon`) | 메인 컴포넌트 Node ID `3716:22984`("내용을 업로드할 때 사용합니다") | +| `Icon/Normal/Trash` | `IconTrash`(`@wanteddev/wds-icon`) | 메인 컴포넌트 Node ID `3562:162692`("내용을 삭제할 때 사용합니다") | +| `Modal`(`3562:162972`) | 공용 `ConfirmModal` | 311×190·radius 24px·버튼 2개로 기존 `Modal`(`995:34701`)과 같은 인스턴스다 | + +### `Image Upload Button`은 WDS가 아니다 + +업로드 버튼(`3716:23609`, 335×46·radius 10px·1px `Line/Normal/Neutral` 테두리 + primary 라벨)은 +`search_design_system`에 같은 모양이 없다. 안쪽 아이콘만 WDS `Icon/Normal/Upload`고 껍데기는 Stream +로컬이다 — WDS `Button`의 outlined는 radius·높이가 달라서 쓰지 않았다. + +### 파일 행 아이콘(`fa7-solid:image`)도 WDS가 아니다 + +업로드한 파일 행의 28px 이미지 아이콘(`3562:162924`)은 이름 그대로 FontAwesome 글리프다. WDS +`IconImage`는 외곽선 스타일이라 **이름은 비슷해도 모양이 달라서** SVG를 받아 +`src/assets/icons/fee/image-file.svg`로 커밋했다. + +### 48px 일러스트는 `export`로 받고 캔버스 배경을 걷어내야 한다 + +`Won Icon`(`3562:163032`)·`Send Fast Icon`(`3562:162980`)은 `svgAssets`가 레이어별로 쪼개져 나와 +`download_assets`의 `export`(format `svg`)를 써야 한다. 그때 붙는 ``와 캔버스 +전체를 덮는 `` 조상 path들을 지우고 아이콘 그룹만 남겼다(사물함 유의사항 일러스트와 같은 처리). + +### 송금 확인 화면의 Sub Action은 `ActionArea variant="strong"`이다 + +주 버튼 아래 텍스트 버튼을 세로로 쌓는 배치(`3562:162974`)는 WDS가 `variant="strong"`일 때만 +`flex-direction: column` + `gap: 8px`가 된다(기본값은 가로 12px). 다만 `ActionAreaButton variant="sub"`가 +세로 margin 8px을 따로 갖고 있어 그대로 두면 간격이 16px이 되므로 `sx`로 `marginBlock: 0`을 준다. +높이도 Figma가 44px인데 WDS는 28px이라 `paddingBlock: "12px"`로 보정한다. + +### 카드 테두리를 `border`로 그리면 안쪽 폭이 2px 깎인다 + +업로드한 이미지 카드(`3562:162918`)는 Figma에서 335×104 안에 303px 행이 들어간다. CSS `border`는 +`box-sizing: border-box` 때문에 가로·세로를 각각 2px씩 먹어서 333/102가 된다 — `outline-1` +`-outline-offset-1`로 그려야 Figma 수치와 맞는다. diff --git a/src/app/router.tsx b/src/app/router.tsx index b1b3b4d..4204d0e 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -12,6 +12,11 @@ import EventsApplicationCompleteScreen from "@/features/events/EventsApplication import EventsApplicationScreen from "@/features/events/EventsApplicationScreen"; import EventsDetailScreen from "@/features/events/EventsDetailScreen"; import EventsListScreen from "@/features/events/EventsListScreen"; +import FeeTransferCompleteScreen from "@/features/fee/FeeTransferCompleteScreen"; +import FeeTransferConfirmScreen from "@/features/fee/FeeTransferConfirmScreen"; +import FeeTransferSemesterScreen from "@/features/fee/FeeTransferSemesterScreen"; +import FeeTransferTossScreen from "@/features/fee/FeeTransferTossScreen"; +import FeeTransferTranscriptScreen from "@/features/fee/FeeTransferTranscriptScreen"; import FeedbacksDetailScreen from "@/features/feedbacks/FeedbacksDetailScreen"; import FeedbacksListScreen from "@/features/feedbacks/FeedbacksListScreen"; import FeedbacksNewScreen from "@/features/feedbacks/FeedbacksNewScreen"; @@ -122,6 +127,53 @@ const routes = [ // 시트만 있는 화면이라 뒤에 Bottom Nav가 비쳐도 Figma(홈 위에 뜨는 시트)와 같다 path: "/lockers/apply", }, + // 학생회비 계좌 송금 플로우 — 전부 하단 고정 버튼(Action Area)이 있는 흰 배경 화면이다. + // 단계를 오갈 수 있어야 해서 한 라우트 위저드가 아니라 단계별 경로로 나눠 뒀다. + { + element: , + // Figma 3562:162833 + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/fee/transfer/semester", + }, + { + element: , + // Figma 3562:162845(업로드 전) · 3562:162891(업로드 후) — 한 화면의 상태 차이다 + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/fee/transfer/transcript", + }, + { + element: , + // Figma 3562:163025(안내) · 3562:163046(이동 실패) — 한 화면의 상태 차이다 + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/fee/transfer/toss", + }, + { + element: , + // Figma 3562:162973 + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/fee/transfer/confirm", + }, + { + element: , + // 신청 결과 화면과 같은 모양이다(Figma 3562:163013) + handle: { + background: "normal", + hasBottomNav: false, + } satisfies ScreenRouteHandle, + path: "/fee/transfer/complete", + }, { element: , // 검색 화면은 Figma에 Bottom Nav가 없고(뒤로가기로 진입한 화면에 복귀) 배경이 흰 면이다 diff --git a/src/assets/icons/fee/image-file.svg b/src/assets/icons/fee/image-file.svg new file mode 100644 index 0000000..b6ee410 --- /dev/null +++ b/src/assets/icons/fee/image-file.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/fee/transcript-example.png b/src/assets/images/fee/transcript-example.png new file mode 100644 index 0000000..ebf6209 Binary files /dev/null and b/src/assets/images/fee/transcript-example.png differ diff --git a/src/assets/lottie/fee/send-fast.json b/src/assets/lottie/fee/send-fast.json new file mode 100644 index 0000000..4905535 --- /dev/null +++ b/src/assets/lottie/fee/send-fast.json @@ -0,0 +1 @@ +{"nm":"keyline-icons:send-fast-duotone","ddd":0,"h":48,"w":48,"meta":{"g":"LottieFiles Figma (Figma Motion)"},"layers":[{"ty":4,"nm":"Send Fast Icon","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Send Fast Icon 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[17,17]},"s":{"a":1,"k":[{"o":{"x":0.22,"y":0.85},"i":{"x":0.35,"y":1},"s":[82,82],"t":0},{"o":{"x":0.65,"y":0},"i":{"x":0.35,"y":1},"s":[103,103],"t":23},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[98,98],"t":36},{"s":[100,100],"t":49}]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[21,27],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[21.77,26.26],"t":1},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[22.48,25.59],"t":2},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[23.1,24.99],"t":3},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[23.62,24.49],"t":4},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.05,24.08],"t":5},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.4,23.74],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.68,23.47],"t":7},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.9,23.26],"t":8},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.08,23.09],"t":9},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.22,22.96],"t":10},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.33,22.85],"t":11},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.42,22.77],"t":12},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.49,22.7],"t":13},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.54,22.65],"t":14},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.59,22.61],"t":15},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.62,22.58],"t":16},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.65,22.55],"t":17},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.67,22.53],"t":18},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.68,22.52],"t":19},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.69,22.51],"t":20},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.7,22.5],"t":21},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.7,22.5],"t":22},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.7,22.5],"t":23},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.69,22.51],"t":24},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.66,22.53],"t":25},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.6,22.57],"t":26},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.5,22.64],"t":27},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.34,22.76],"t":28},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25.08,22.96],"t":29},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.76,23.19],"t":30},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.52,23.36],"t":31},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.37,23.47],"t":32},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.28,23.54],"t":33},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.23,23.58],"t":34},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.21,23.59],"t":35},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.2,23.6],"t":36},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.37,23.47],"t":37},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.51,23.37],"t":38},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.63,23.28],"t":39},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.73,23.2],"t":40},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.81,23.15],"t":41},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.87,23.1],"t":42},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.91,23.06],"t":43},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.95,23.04],"t":44},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.97,23.02],"t":45},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[24.99,23.01],"t":46},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":47},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":48},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":49},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":50},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":51},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":52},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":53},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":54},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":55},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":56},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":57},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":58},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":59},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":60},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":61},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":62},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":63},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":64},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":65},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":66},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":67},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":68},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":69},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":70},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":71},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":72},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":73},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":74},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":75},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":76},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":77},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":78},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":79},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":80},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":81},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":82},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":83},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":84},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":85},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":86},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":87},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":88},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":89},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":90},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":91},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":92},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":93},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":94},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":95},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":96},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":97},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":98},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":99},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":100},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":101},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":102},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":103},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":104},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":105},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":106},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":107},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":108},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":109},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":110},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":111},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":112},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":113},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":114},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":115},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":116},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":117},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":118},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[25,23],"t":119},{"s":[25,23],"t":120}]},"r":{"a":1,"k":[{"o":{"x":0.22,"y":0.85},"i":{"x":0.35,"y":1},"s":[6],"t":0},{"o":{"x":0.65,"y":0},"i":{"x":0.35,"y":1},"s":[-2.5],"t":23},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[1],"t":36},{"s":[0],"t":49}]},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[0],"t":0},{"s":[100],"t":22}]}},"shapes":[],"ind":1},{"ty":4,"nm":"Transfer Illustration Path 3","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Transfer Illustration Path 3 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[7.69,7.69]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[7.69,26.31]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.2,"y":0},"i":{"x":0.2,"y":1},"s":[0],"t":0},{"s":[100],"t":24}]}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,15.38],[7.29,8.1]]}}},{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,5.67],[5.67,0]]}}},{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[9.71,15.38],[15.38,9.71]]}}},{"ty":"st","bm":0,"hd":false,"nm":"","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":3.238095283508301},"c":{"a":0,"k":[0,0.4,1]}}],"ind":2,"parent":1},{"ty":4,"nm":"Transfer Illustration Path 2","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Transfer Illustration Path 2 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[9.54,14.3]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[24.46,14.77]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-0.43],[0.04,-0.15],[0,0],[1.43,0],[0.6,0.88],[0,0],[0.14,0.14],[0,0]],"o":[[0.3,0.3],[0,0.16],[0,0],[-0.41,1.38],[-1.07,0],[0,0],[-0.11,-0.17],[0,0],[0,0]],"v":[[18.6,0],[19.07,1.14],[19,1.6],[11.71,26.27],[8.6,28.59],[5.93,27.18],[0.38,19.06],[0,18.6],[18.6,0]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0,0.4,1]},"r":1,"o":{"a":0,"k":100}}],"ind":3,"parent":1},{"ty":4,"nm":"Transfer Illustration Path 1","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Transfer Illustration Path 1 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[14.53,14.53]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[19.47,14.53]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[1.43,0],[0.6,0.88],[0,0],[0.33,0.23],[0,0],[0,1.07],[-1.38,0.41],[0,0],[-0.16,0],[0,-0.89],[0.04,-0.15]],"o":[[0,0],[-0.41,1.38],[-1.07,0],[0,0],[-0.23,-0.33],[0,0],[-0.88,-0.6],[0,-1.43],[0,0],[0.15,-0.04],[0.89,0],[0,0.16],[0,0]],"v":[[29,2.08],[21.7,26.75],[18.6,29.07],[15.92,27.65],[10.38,19.54],[9.53,18.69],[1.41,13.14],[0,10.47],[2.32,7.36],[26.99,0.07],[27.45,0],[29.07,1.62],[29,2.08]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0.6196,0.7725,1]},"r":1,"o":{"a":0,"k":100}}],"ind":4,"parent":1}],"v":"5.7.0","fr":60,"op":120,"ip":0,"assets":[]} \ No newline at end of file diff --git a/src/assets/lottie/fee/won.json b/src/assets/lottie/fee/won.json new file mode 100644 index 0000000..a4b4afe --- /dev/null +++ b/src/assets/lottie/fee/won.json @@ -0,0 +1 @@ +{"nm":"Won Icon","ddd":0,"h":48,"w":48,"meta":{"g":"LottieFiles Figma (Figma Motion)"},"layers":[{"ty":4,"nm":"Won Icon","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Icon 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[20,20]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[24,24]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[],"ind":1},{"ty":4,"nm":"Won Symbol","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Symbol 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[9.62,7.13]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[19.62,21.15]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[],"ind":2,"parent":1},{"ty":4,"nm":"Won Symbol Stroke 2","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Symbol Stroke 2 1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[9.62,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[9.62,4.96]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[19.23,0]]}}},{"ty":"tm","bm":0,"hd":false,"nm":"","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"o":{"x":0.25,"y":0.65},"i":{"x":0.35,"y":1},"s":[0],"t":25},{"s":[100],"t":66}]},"o":{"a":0,"k":0},"s":{"a":0,"k":0},"m":1},{"ty":"st","bm":0,"hd":false,"nm":"","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":2.7295289039611816},"c":{"a":0,"k":[1,1,1]}}],"ind":3,"parent":2},{"ty":4,"nm":"Won Symbol Stroke 1","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Symbol Stroke 1 1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[7.83,7.13]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[9.69,7.13]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0,0],[3.72,14.27],[8.06,0],[11.88,14.27],[15.65,0]]}}},{"ty":"tm","bm":0,"hd":false,"nm":"","e":{"a":1,"k":[{"o":{"x":0.25,"y":0.65},"i":{"x":0.35,"y":1},"s":[0],"t":0},{"s":[100],"t":66}]},"o":{"a":0,"k":0},"s":{"a":0,"k":0},"m":1},{"ty":"st","bm":0,"hd":false,"nm":"","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":2.7295289039611816},"c":{"a":0,"k":[1,1,1]}}],"ind":4,"parent":2},{"ty":4,"nm":"Won Icon Background 1","sr":1,"st":0,"op":121,"ip":0,"hd":false,"ln":"Won Icon Background 1 0","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[20,20]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[20,20]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[11.05,0],[0,11.05],[-11.05,0],[0,-11.05]],"o":[[0,11.05],[-11.05,0],[0,-11.05],[11.05,0],[0,0]],"v":[[40,20],[20,40],[0,20],[20,0],[40,20]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":0,"k":[0.9961,0.8118,0.3608]},"r":1,"o":{"a":0,"k":100}}],"ind":5,"parent":1}],"v":"5.7.0","fr":60,"op":120,"ip":0,"assets":[]} \ No newline at end of file diff --git a/src/components/ui/ConfirmModal.tsx b/src/components/ui/ConfirmModal.tsx index 2f9ae8a..8b020fe 100644 --- a/src/components/ui/ConfirmModal.tsx +++ b/src/components/ui/ConfirmModal.tsx @@ -165,8 +165,11 @@ function ConfirmModal({ {title} {description && ( + // Figma 모달 설명은 줄바꿈 위치까지 디자인에 포함돼 있어서(예: K스마트멘토 이동 + // 확인 3562:162972) `\n`을 그대로 살린다. 기존 사용처는 모두 한 줄 문구라 영향이 없다. {title} + {/* Figma 설명 문구는 줄바꿈 위치까지 디자인에 포함돼 있어서(예: 송금 확인요청 완료 + 3562:163013) `\n`을 그대로 살린다. 기존 사용처는 모두 한 줄 문구라 영향이 없다. */} navigate("/", { replace: true }); + + return ( + } + illustrationGap={8} + onClose={goHome} + primaryAction={{ label: "홈으로 가기", onClick: goHome }} + title="송금 확인을 요청했어요" + /> + ); +} + +export default FeeTransferCompleteScreen; diff --git a/src/features/fee/FeeTransferConfirmScreen.tsx b/src/features/fee/FeeTransferConfirmScreen.tsx new file mode 100644 index 0000000..ef40df2 --- /dev/null +++ b/src/features/fee/FeeTransferConfirmScreen.tsx @@ -0,0 +1,53 @@ +import { ActionAreaButton } from "@wanteddev/wds"; +import { useNavigate } from "react-router-dom"; + +import sendFastMotion from "@/assets/lottie/fee/send-fast.json"; +import FeeLottieIcon from "@/features/fee/components/FeeLottieIcon"; +import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; +import { FEE_TRANSFER_PATHS } from "@/features/fee/constants/fee"; + +// Figma: 계좌 송금 - 토스 다녀와서 확인 버튼 (nodeId 3562:162973) +// 토스에서 송금하고 돌아온 사용자에게 확인을 받는 단계. 실제 입금 확인은 학생회가 수동으로 +// 하기 때문에 여기서 "네"를 눌러야 확인 요청이 접수된다. +// +// 종이비행기(3562:162980)는 기울기·크기·위치가 함께 튀어 들어오는(overshoot) Figma Motion이라 +// Lottie로 받아 재생한다. 플러그인이 이징을 매 프레임으로 구워서 키프레임만 보면 끝을 알 수 +// 없는데, 실제 값은 49프레임(0.82초)부터 고정이다 — rotation 6° → -2.5° → 1° → 0°로 잦아든다. +function FeeTransferConfirmScreen() { + const navigate = useNavigate(); + + return ( + + + navigate(FEE_TRANSFER_PATHS.complete, { replace: true }) + } + sx={{ paddingBlock: "16px" }} + > + 네, 송금했어요 + + {/* 송금을 미루고 빠져나가는 길 — 플로우를 처음부터 다시 타도록 홈으로 보낸다. + Figma Sub Action은 44px인데 WDS 텍스트 버튼은 28px이라 세로 padding을 보정하고, + WDS가 따로 주는 세로 margin 8px은 지운다(안 지우면 Figma의 8px 간격이 16px이 된다). */} + navigate("/")} + sx={{ marginBlock: 0, paddingBlock: "12px" }} + variant="sub" + > + 다음에 할게요 + + + } + actionsVariant="strong" + contentGap={12} + illustration={ + + } + title="송금을 완료하셨나요?" + /> + ); +} + +export default FeeTransferConfirmScreen; diff --git a/src/features/fee/FeeTransferSemesterScreen.tsx b/src/features/fee/FeeTransferSemesterScreen.tsx new file mode 100644 index 0000000..d0413b7 --- /dev/null +++ b/src/features/fee/FeeTransferSemesterScreen.tsx @@ -0,0 +1,56 @@ +import { ActionAreaButton, TextField } from "@wanteddev/wds"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; +import { FEE_TRANSFER_PATHS } from "@/features/fee/constants/fee"; + +// 남은 학기 수로 받을 수 있는 값. 0학기는 납부할 이유가 없고, 상한은 실제 학적 기준이 없어 +// 두 자리까지만 받는다 — 서버 검증 규칙이 정해지면 맞춘다. +const SEMESTER_PATTERN = /^[1-9][0-9]?$/; + +// Figma: 계좌 송금 - 남은 학기 수 (nodeId 3562:162833) +// 숫자만 받고, 유효한 값이 들어왔을 때만 "다음"이 활성화된다(Figma 기본 상태가 비활성). +function FeeTransferSemesterScreen() { + const navigate = useNavigate(); + const [semester, setSemester] = useState(""); + const isValid = SEMESTER_PATTERN.test(semester); + + return ( + + navigate(FEE_TRANSFER_PATHS.transcript, { + state: { semester: Number(semester) }, + }) + } + sx={{ paddingBlock: "16px" }} + > + 다음 + + } + description="남은 학기 수에 따라 납부 금액이 달라져요." + title={ + <> + 현재 남은 학기 수를 +
+ 알려 주세요 + + } + > + + setSemester(event.target.value.replace(/[^0-9]/g, "").slice(0, 2)) + } + placeholder="ex) 3" + value={semester} + /> +
+ ); +} + +export default FeeTransferSemesterScreen; diff --git a/src/features/fee/FeeTransferTossScreen.tsx b/src/features/fee/FeeTransferTossScreen.tsx new file mode 100644 index 0000000..60284fe --- /dev/null +++ b/src/features/fee/FeeTransferTossScreen.tsx @@ -0,0 +1,92 @@ +import { ActionAreaButton } from "@wanteddev/wds"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import wonMotion from "@/assets/lottie/fee/won.json"; +import FeeLottieIcon from "@/features/fee/components/FeeLottieIcon"; +import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; +import { + FEE_EXTERNAL_LINKS, + FEE_TRANSFER_PATHS, +} from "@/features/fee/constants/fee"; + +// Figma: 계좌 송금 - 토스로 이동 안내 (nodeId 3562:163025) / 토스 이동 실패 (3562:163046) +// +// 두 프레임은 같은 화면의 상태 차이다 — 실패하면 일러스트가 빠지고 문구와 버튼 라벨만 바뀐다. +// 그래서 라우트를 나누지 않고 실패 여부 state로 토글한다. +// +// 토스는 딥링크라 열렸는지 브라우저가 알려주지 않는다. 링크 주소 자체가 아직 없어서 지금은 +// 상수가 비어 있으면 곧바로 실패 상태로 두고, 주소가 생기면 window.open 결과로 판정한다. +// +// ₩ 아이콘(3562:163032)은 획이 그려지는 Figma Motion이라 Lottie로 받아 재생한다. 2초 +// 타임라인이지만 실제 움직임은 66프레임(1.1초)에 끝나고 나머지는 멈춰 있는 구간이다 — +// json의 키프레임이 실제로 66에서 끝나는 걸 확인했다. +function FeeTransferTossScreen() { + const navigate = useNavigate(); + const [hasFailed, setFailed] = useState(false); + + const handleOpenToss = () => { + if (!FEE_EXTERNAL_LINKS.tossTransfer) { + setFailed(true); + return; + } + const opened = window.open( + FEE_EXTERNAL_LINKS.tossTransfer, + "_blank", + "noopener", + ); + if (opened) { + // 토스에 다녀오면 돌아올 화면이 송금 확인 단계다 + navigate(FEE_TRANSFER_PATHS.confirm); + return; + } + setFailed(true); + }; + + if (hasFailed) { + return ( + + 토스로 다시 이동하기 + + } + contentGap={8} + description={ + "잠시 후 다시 시도해 주세요.\n계속 열리지 않으면 토스 앱을 확인해 주세요." + } + title="토스를 열지 못했어요" + /> + ); + } + + return ( + + 토스로 이동하기 + + } + contentGap={8} + description={ + "송금 후 이 화면으로 돌아와\n송금 완료 여부를 꼭 확인해 주세요." + } + illustration={} + title={ + <> + 이제 토스에서 학생회비를 +
+ 송금해 주세요 + + } + /> + ); +} + +export default FeeTransferTossScreen; diff --git a/src/features/fee/FeeTransferTranscriptScreen.tsx b/src/features/fee/FeeTransferTranscriptScreen.tsx new file mode 100644 index 0000000..3ecbdaf --- /dev/null +++ b/src/features/fee/FeeTransferTranscriptScreen.tsx @@ -0,0 +1,118 @@ +import { ActionAreaButton, Typography } from "@wanteddev/wds"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import ConfirmModal from "@/components/ui/ConfirmModal"; +import FeeImagePreviewOverlay from "@/features/fee/components/FeeImagePreviewOverlay"; +import FeeImageUploadButton from "@/features/fee/components/FeeImageUploadButton"; +import FeeTranscriptGuide from "@/features/fee/components/FeeTranscriptGuide"; +import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; +import FeeUploadedFileCard from "@/features/fee/components/FeeUploadedFileCard"; +import { + FEE_EXTERNAL_LINKS, + FEE_TRANSFER_PATHS, +} from "@/features/fee/constants/fee"; + +// Figma: 계좌 송금 - 이미지 업로드하기 전 (nodeId 3562:162845) / 업로드 후 (3562:162891) / +// K-스마트멘토 이동 확인 모달 (3562:162940) / 업로드한 이미지 미리보기 (3595:70574) +// +// 네 프레임이 한 화면의 상태 변화라 라우트를 나누지 않고 업로드 여부·모달 열림으로 토글한다. +// 업로드 API가 아직 없어서 고른 파일을 그대로 들고만 있는다 — 미리보기는 objectURL로 띄우고, +// 연동할 때 이 자리에서 업로드 요청을 보내면 된다. +function FeeTransferTranscriptScreen() { + const navigate = useNavigate(); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(""); + const [isPreviewOpen, setPreviewOpen] = useState(false); + const [isGuideModalOpen, setGuideModalOpen] = useState(false); + + // objectURL은 안 풀어주면 파일을 바꿔 고를 때마다 메모리에 쌓인다 + useEffect(() => { + if (!file) { + setPreviewUrl(""); + return; + } + const url = URL.createObjectURL(file); + setPreviewUrl(url); + + return () => URL.revokeObjectURL(url); + }, [file]); + + const handleGuideConfirm = () => { + setGuideModalOpen(false); + if (FEE_EXTERNAL_LINKS.kSmartMentor) { + window.open(FEE_EXTERNAL_LINKS.kSmartMentor, "_blank", "noopener"); + } + }; + + return ( + <> + navigate(FEE_TRANSFER_PATHS.toss)} + sx={{ paddingBlock: "16px" }} + > + 다음 + + } + description="홈 > ‘학점이수현황 자세히 보기’에 있어요." + title={ + <> + {/* Figma에서 "K스마트멘토↗"만 primary + 밑줄이고, 누르면 이동 확인 모달이 뜬다 */} + + 에서 +
+ 학점이수현황을 캡처해 주세요 + + } + > + + {file ? ( + setPreviewOpen(true)} + onRemove={() => setFile(null)} + /> + ) : ( + + )} +
+ + setGuideModalOpen(false)} + onConfirm={handleGuideConfirm} + open={isGuideModalOpen} + title="K스마트멘토로 이동할까요?" + /> + + setPreviewOpen(false)} + open={isPreviewOpen && Boolean(previewUrl)} + src={previewUrl} + /> + + ); +} + +export default FeeTransferTranscriptScreen; diff --git a/src/features/fee/components/FeeImagePreviewOverlay.tsx b/src/features/fee/components/FeeImagePreviewOverlay.tsx new file mode 100644 index 0000000..2bcee57 --- /dev/null +++ b/src/features/fee/components/FeeImagePreviewOverlay.tsx @@ -0,0 +1,65 @@ +import { TopNavigationButton } from "@wanteddev/wds"; +import { IconClose } from "@wanteddev/wds-icon"; +import { useEffect } from "react"; +import { createPortal } from "react-dom"; + +import { useScreenSheetPortal } from "@/components/ui/useScreenSheetPortal"; + +interface FeeImagePreviewOverlayProps { + open: boolean; + src: string; + alt: string; + onClose: () => void; +} + +// Figma: 업로드한 이미지 미리보기 (nodeId 3595:70574) +// 업로드한 캡처를 전체 화면으로 키워 보는 오버레이. 헤더가 X 하나뿐이라 화면 라우트로 두지 않고 +// 업로드 화면 위에 포털로 띄운다 — 뒤로가기로 업로드 단계가 사라지면 안 되기 때문이다. +function FeeImagePreviewOverlay({ + open, + src, + alt, + onClose, +}: FeeImagePreviewOverlayProps) { + const portalEl = useScreenSheetPortal(); + + // 이미지 뷰어는 Esc로 닫는 게 기본 동작이라 모달과 같이 맞춘다 + useEffect(() => { + if (!open) { + return; + } + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + document.addEventListener("keydown", handleKeyDown); + + return () => document.removeEventListener("keydown", handleKeyDown); + }, [open, onClose]); + + if (!portalEl || !open) { + return null; + } + + return createPortal( +
+
+ + + +
+
+ {alt} +
+
, + portalEl, + ); +} + +export default FeeImagePreviewOverlay; diff --git a/src/features/fee/components/FeeImageUploadButton.tsx b/src/features/fee/components/FeeImageUploadButton.tsx new file mode 100644 index 0000000..420a300 --- /dev/null +++ b/src/features/fee/components/FeeImageUploadButton.tsx @@ -0,0 +1,52 @@ +import { Typography } from "@wanteddev/wds"; +import { IconUpload } from "@wanteddev/wds-icon"; +import { useId } from "react"; + +import { FEE_TRANSCRIPT_ACCEPT } from "@/features/fee/constants/fee"; + +interface FeeImageUploadButtonProps { + onSelect: (file: File) => void; +} + +// Figma: Image Upload Button (nodeId 3716:23609) — WDS가 아니라 Stream 로컬 컴포넌트다. +// `search_design_system`에 같은 모양의 아웃라인 버튼이 없고, 안쪽 아이콘만 WDS +// `Icon/Normal/Upload`(3716:22984)다. WDS Button의 outlined는 radius·높이가 달라서 쓰지 않는다. +// +// 파일 선택은 보이지 않는 에 위임한다 — label로 감싸면 버튼 모양을 그대로 +// 두면서도 키보드·스크린리더에서 하나의 컨트롤로 읽힌다. +function FeeImageUploadButton({ onSelect }: FeeImageUploadButtonProps) { + const inputId = useId(); + + return ( + + ); +} + +export default FeeImageUploadButton; diff --git a/src/features/fee/components/FeeLottieIcon.tsx b/src/features/fee/components/FeeLottieIcon.tsx new file mode 100644 index 0000000..23c8538 --- /dev/null +++ b/src/features/fee/components/FeeLottieIcon.tsx @@ -0,0 +1,44 @@ +import { LottieLight } from "lottie-react"; + +import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; + +interface FeeLottieIconProps { + /** LottieFiles 플러그인으로 받은 Figma Motion json */ + animation: object; + /** + * 움직임이 끝나 값이 고정되는 프레임. 타임라인은 2초지만 실제 움직임은 앞부분에서 끝나고 + * 나머지는 멈춰 있는 구간이라, 모션 줄이기일 때 이 한 장만 세워 둔다. + */ + settledFrame: number; + /** 스크린리더에 읽힐 이름. 장식이면 생략한다 */ + label?: string; +} + +// 송금 플로우의 48px 일러스트 두 개(₩ 3562:163032, 종이비행기 3562:162980)가 공유하는 재생기. +// 둘 다 Figma Motion이 붙어 있어 키프레임을 손으로 옮기지 않고 Lottie로 받아 재생한다 +// (component-convention.md "모션 (Lottie)"). +// +// `LottieLight`를 쓰는 이유는 완료 체크(CompleteCheck)와 같다 — 풀 빌드는 canvas·HTML 렌더러와 +// `eval` 기반 표현식 엔진까지 끌고 와서 번들이 커지고 WebView CSP에서 깨질 수 있다. +function FeeLottieIcon({ animation, settledFrame, label }: FeeLottieIconProps) { + const shouldReduceMotion = usePrefersReducedMotion(); + + return ( + + ); +} + +export default FeeLottieIcon; diff --git a/src/features/fee/components/FeeTranscriptGuide.tsx b/src/features/fee/components/FeeTranscriptGuide.tsx new file mode 100644 index 0000000..fdfd281 --- /dev/null +++ b/src/features/fee/components/FeeTranscriptGuide.tsx @@ -0,0 +1,43 @@ +import { Typography } from "@wanteddev/wds"; + +import transcriptExample from "@/assets/images/fee/transcript-example.png"; + +// Figma: Transcript Preview Section (nodeId 3562:162857) +// 어떤 화면을 캡처해야 하는지 보여주는 예시 카드. +// +// 안쪽 그림(3562:162858)은 Figma에서 실제 K-스마트멘토 스크린샷 위에 가림 박스 9개와 가짜 값 +// (소프트웨어전공 / 20260000 / 홍길동 등)을 얹어 만든 것이다. 그래서 레이어별로 쪼개진 +// `svgAssets`나 원본 `rawImages`를 쓰면 안 되고, 가림까지 합쳐진 `export` PNG만 커밋한다 — +// 커밋 전에 가림 박스가 전부 불투명한지(밑글씨가 비치지 않는지) 픽셀로 확인했다. +// 3배 크기(840x615)로 받아서 고해상도 화면에서도 또렷하게 보이게 한다. +function FeeTranscriptGuide() { + return ( + // Figma 카드는 248px에서 잘린다 — 예시 이미지 아래쪽(이수학기 행)이 일부러 잘려 보인다 + // 카드 높이를 고정하지 않는다 — 화면 프레임이 w-full / sm:w-[480px]라 카드 폭이 변하는데, + // 248px(375 기준)로 박으면 넓은 화면에서 그림 아래가 잘린다. 아래 여백 없이 그림이 카드 + // 바닥에 붙는 Figma 구조(3562:162857: 위 16px, 내용 231.76px, 합 248px) 그대로 내용이 + // 높이를 정하게 두면 설계 폭에서 자동으로 248px이 된다. 모서리 때문에 overflow만 숨긴다. +
+ + 캡처 예시 + + {/* Figma 그림 크기는 280px 고정이다. 카드가 그보다 좁아지면 Tailwind preflight의 + max-width:100%가 알아서 줄여준다 — 넓어져도 커지지는 않게 w-full을 쓰지 않는다. */} + 학점이수현황 캡처 예시 — 학생정보와 이수학기가 보이는 화면 +
+ ); +} + +export default FeeTranscriptGuide; diff --git a/src/features/fee/components/FeeTransferStepLayout.tsx b/src/features/fee/components/FeeTransferStepLayout.tsx new file mode 100644 index 0000000..652f6c4 --- /dev/null +++ b/src/features/fee/components/FeeTransferStepLayout.tsx @@ -0,0 +1,114 @@ +import { ActionArea, TopNavigationButton, Typography } from "@wanteddev/wds"; +import { IconChevronLeft } from "@wanteddev/wds-icon"; +import type { ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; + +import ScreenHeader from "@/components/ui/ScreenHeader"; +import { useScreenHeader } from "@/components/ui/useScreenHeader"; + +// 본문 스택 간격. Figma가 화면마다 다르게 잡아뒀다 — +// 토스 안내·실패는 8px(3562:163031), 송금 확인은 12px(3562:162979), +// 입력이 있는 단계는 16px(3562:162839, 3562:162853). +type FeeTransferContentGap = 8 | 12 | 16; + +const CONTENT_GAP_CLASS_NAMES: Record = { + 8: "gap-2", + 12: "gap-3", + 16: "gap-4", +}; + +interface FeeTransferStepLayoutProps { + /** 제목 위에 놓이는 48px 일러스트 */ + illustration?: ReactNode; + title: ReactNode; + description?: ReactNode; + contentGap?: FeeTransferContentGap; + /** 제목/설명 아래에 붙는 입력·카드 등 */ + children?: ReactNode; + /** ActionArea 안에 들어갈 버튼들 */ + actions: ReactNode; + /** + * 주 버튼 아래에 텍스트 버튼을 세로로 쌓을 때(송금 확인 화면) `"strong"`을 준다 — + * WDS가 그때만 세로 배치 + 8px 간격이 되어 Figma Contents(3562:162974)와 맞는다. + */ + actionsVariant?: "strong"; +} + +// Figma: 계좌 송금 플로우 5개 화면이 공유하는 뼈대 — +// 남은 학기 수(3562:162833) / 학점이수현황 업로드(3562:162845) / +// 토스 이동 안내(3562:163025) / 토스 이동 실패(3562:163046) / 송금 확인(3562:162973). +// +// 다섯 화면 모두 `뒤로가기만 있는 헤더 → 12px → (일러스트) 제목·설명 → 본문 → 하단 고정 버튼` +// 구조가 같고 간격과 문구만 다르다. 제목은 Label/Strong(#000000)으로, 설명은 +// Label/Alternative로 고정이라 prop으로 빼지 않았다. +// +// 완료 화면(3562:163013)만 이 뼈대를 쓰지 않는다 — 뒤로가기가 아니라 X로 닫고 중앙 정렬이라 +// 신청 결과 화면 공용 컴포넌트(ResultScreen)와 같은 모양이다. +function FeeTransferStepLayout({ + illustration, + title, + description, + contentGap = 16, + children, + actions, + actionsVariant, +}: FeeTransferStepLayoutProps) { + const navigate = useNavigate(); + + useScreenHeader( + navigate(-1)} + variant="icon" + > + + + } + variant="normal" + />, + ); + + return ( +
+ {/* Figma Student Fee Transfer Content: 헤더 아래 12px */} +
+ {illustration} +
+ + {title} + + {description && ( + // Figma 설명 문구는 줄바꿈 위치까지 디자인에 포함돼 있다 + + {description} + + )} +
+ {children} +
+ +
+ {actions} + {/* 다른 하단 고정 버튼 화면과 같은 14px — WDS ActionArea 아래 padding 20px에 Home Bar 여백을 더해 34px */} +
+
+
+ ); +} + +export default FeeTransferStepLayout; diff --git a/src/features/fee/components/FeeUploadedFileCard.tsx b/src/features/fee/components/FeeUploadedFileCard.tsx new file mode 100644 index 0000000..afa0873 --- /dev/null +++ b/src/features/fee/components/FeeUploadedFileCard.tsx @@ -0,0 +1,67 @@ +import { Typography } from "@wanteddev/wds"; +import { IconTrash } from "@wanteddev/wds-icon"; + +import imageFileIcon from "@/assets/icons/fee/image-file.svg"; + +interface FeeUploadedFileCardProps { + fileName: string; + /** 파일 행을 누르면 원본을 크게 본다(업로드한 이미지 미리보기 3595:70574) */ + onPreview: () => void; + onRemove: () => void; +} + +// Figma: Uploaded Image (nodeId 3562:162918) +// 흰 카드 + 1px 테두리 안에 파일 행 하나가 들어간다. 행 배경 Atomic/Blue/99(#F7FBFF)는 +// primary-subtle(Blue/95)보다 훨씬 옅은 별개 값이라 index.css에 토큰을 따로 추가했다. +// +// 파일 아이콘(28px)은 WDS가 아니라 Figma가 쓴 `fa7-solid:image` 글리프다 — WDS `IconImage`는 +// 외곽선 스타일이라 모양이 달라서 SVG를 받아 커밋했다. +function FeeUploadedFileCard({ + fileName, + onPreview, + onRemove, +}: FeeUploadedFileCardProps) { + return ( + // 테두리는 border가 아니라 outline으로 그린다 — Figma는 335×104 안쪽에 303px 행을 두는데, + // CSS border는 box-sizing 때문에 가로·세로를 각각 2px씩 깎아먹는다 +
+ + 업로드한 이미지 + +
+ + +
+
+ ); +} + +export default FeeUploadedFileCard; diff --git a/src/features/fee/constants/fee.ts b/src/features/fee/constants/fee.ts new file mode 100644 index 0000000..9d3a6a2 --- /dev/null +++ b/src/features/fee/constants/fee.ts @@ -0,0 +1,23 @@ +// 학생회비 계좌 송금 플로우 경로. 화면설계서(687:20584)에는 경로 표기 컬럼이 아예 없어서 +// 용어 사전(terminology.md)의 `회비 납부 → fee`와 "프론트 라우트 경로: /{코드 용어}" 규칙으로 정했다. +export const FEE_TRANSFER_PATHS = { + complete: "/fee/transfer/complete", + confirm: "/fee/transfer/confirm", + semester: "/fee/transfer/semester", + toss: "/fee/transfer/toss", + transcript: "/fee/transfer/transcript", +} as const; + +// 아직 받지 못한 외부 연동 주소들. 값이 있을 때만 열도록 해서 빈 창이 뜨는 걸 막는다 +// (설정 화면의 SETTINGS_EXTERNAL_LINKS와 같은 방식). +// +// - kSmartMentor: 학점이수현황을 확인하는 국민대 K-스마트멘토 주소 +// - tossTransfer: 계좌·금액이 채워진 토스 송금 딥링크. 학생회 계좌와 납부 금액 규칙이 정해져야 +// 만들 수 있다(남은 학기 수에 따라 금액이 달라진다). +export const FEE_EXTERNAL_LINKS = { + kSmartMentor: "", + tossTransfer: "", +} as const; + +// 업로드 받는 이미지 형식 — 학점이수현황 "캡처"라 스크린샷 확장자만 받는다 +export const FEE_TRANSCRIPT_ACCEPT = "image/png,image/jpeg,image/webp"; diff --git a/src/index.css b/src/index.css index 14d7685..fda6efe 100644 --- a/src/index.css +++ b/src/index.css @@ -49,6 +49,10 @@ /* 사물함 칸 선택 — 선택 가능한 칸 배경(Figma "Orange/95")과 칸 묶음 테두리(Figma "Line/Solid/Alternative") */ --color-orange-95: var(--atomic-orange-95); + + /* 학생회비 송금 — 업로드한 파일 행 배경(Figma "Atomic/Blue/99", 3562:162922). + primary-subtle(Blue/95)보다 훨씬 옅어서 흰 카드 위에서 행만 살짝 떠 보인다 */ + --color-blue-99: var(--atomic-blue-99); --color-line-solid-alternative: var(--semantic-line-solid-alternative); --color-status-positive: var(--semantic-status-positive);