From 94c4211fc00a762d825592ecd4d7f3412dc8eabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:33:32 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20=EA=B3=B5=EC=9A=A9=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=C2=B7=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EC=9D=98=20=EC=A4=84=EB=B0=94=EA=BF=88?= =?UTF-8?q?=EC=9D=B4=20=EB=AC=B4=EC=8B=9C=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Figma 설명 문구는 줄바꿈 위치까지 디자인에 포함돼 있는데 `\n`이 공백으로 접혀서 의도와 다른 자리에서 줄이 바뀌었다. 기존 사용처는 모두 한 줄 문구라 렌더링 변화가 없다. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/ui/ConfirmModal.tsx | 3 +++ src/components/ui/ResultScreen.tsx | 3 +++ 2 files changed, 6 insertions(+) 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`을 그대로 살린다. 기존 사용처는 모두 한 줄 문구라 영향이 없다. */} Date: Thu, 8 Oct 2026 16:33:32 +0900 Subject: [PATCH 2/9] =?UTF-8?q?chore:=20=ED=95=99=EC=83=9D=ED=9A=8C?= =?UTF-8?q?=EB=B9=84=20=EC=86=A1=EA=B8=88=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=97=90=EC=85=8B=EA=B3=BC=20=EC=83=89=EC=83=81=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 48px 일러스트 둘은 svgAssets가 레이어별로 쪼개져 나와 export(svg)로 받고, 같이 붙어 나온 캔버스 배경 rect와 조상 path를 걷어냈다. 파일 행 아이콘은 WDS IconImage와 글리프가 달라서 Figma 것을 그대로 받았다. 업로드한 파일 행 배경(Atomic/Blue/99)은 기존 primary-subtle(Blue/95)보다 훨씬 옅은 별개 값이라 토큰을 새로 연결한다. Co-Authored-By: Claude Opus 5 (1M context) --- src/assets/icons/fee/image-file.svg | 5 +++++ src/assets/icons/fee/send-fast.svg | 9 +++++++++ src/assets/icons/fee/won.svg | 11 +++++++++++ src/assets/images/fee/transcript-example.png | Bin 0 -> 9994 bytes src/index.css | 4 ++++ 5 files changed, 29 insertions(+) create mode 100644 src/assets/icons/fee/image-file.svg create mode 100644 src/assets/icons/fee/send-fast.svg create mode 100644 src/assets/icons/fee/won.svg create mode 100644 src/assets/images/fee/transcript-example.png 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/icons/fee/send-fast.svg b/src/assets/icons/fee/send-fast.svg new file mode 100644 index 0000000..95fa6f3 --- /dev/null +++ b/src/assets/icons/fee/send-fast.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/fee/won.svg b/src/assets/icons/fee/won.svg new file mode 100644 index 0000000..5de700c --- /dev/null +++ b/src/assets/icons/fee/won.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/fee/transcript-example.png b/src/assets/images/fee/transcript-example.png new file mode 100644 index 0000000000000000000000000000000000000000..5ffff003fdc803bd5288fe2a97e0ea6e8f8587f7 GIT binary patch literal 9994 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 RtW*t7PD)9#QrtNBKLAQ@0!#n^ literal 0 HcmV?d00001 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); From f5f4c185efabc08cc43687998b1af40a1d345d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:33:32 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=ED=95=99=EC=83=9D=ED=9A=8C?= =?UTF-8?q?=EB=B9=84=20=EA=B3=84=EC=A2=8C=20=EC=86=A1=EA=B8=88=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 홈의 "학생회비 납부하기"에서 시작하는 5단계 송금 플로우다. PG 결제가 아니라 토스 딥링크로 송금하고 학생회가 수동으로 입금을 확인하는 방식이라, 남은 학기 수와 학점이수현황 캡처를 받은 뒤 토스로 보냈다가 돌아와 확인을 받는다. 다섯 화면이 `뒤로가기 헤더 → 12px → (일러스트) 제목·설명 → 본문 → 하단 고정 버튼` 구조가 같고 간격과 문구만 달라서 FeeTransferStepLayout으로 묶었다. 업로드 전/후와 토스 안내/실패는 각각 한 화면의 상태 차이라 라우트를 나누지 않았고, 확인요청 완료 화면은 체크 모션이 신청 완료 화면들과 같은 Figma 메인 컴포넌트라 공용 ResultScreen을 쓴다. 경로는 화면설계서에 표기가 없어 용어 사전의 `회비 납부 → fee`로 정했다. Co-Authored-By: Claude Opus 5 (1M context) --- docs/conventions/wds-component-usage.md | 40 ++++++ src/app/router.tsx | 52 ++++++++ .../fee/FeeTransferCompleteScreen.tsx | 27 ++++ src/features/fee/FeeTransferConfirmScreen.tsx | 51 ++++++++ .../fee/FeeTransferSemesterScreen.tsx | 56 +++++++++ src/features/fee/FeeTransferTossScreen.tsx | 87 +++++++++++++ .../fee/FeeTransferTranscriptScreen.tsx | 118 ++++++++++++++++++ .../fee/components/FeeImagePreviewOverlay.tsx | 65 ++++++++++ .../fee/components/FeeImageUploadButton.tsx | 52 ++++++++ .../fee/components/FeeTranscriptGuide.tsx | 30 +++++ .../fee/components/FeeTransferStepLayout.tsx | 114 +++++++++++++++++ .../fee/components/FeeUploadedFileCard.tsx | 67 ++++++++++ src/features/fee/constants/fee.ts | 23 ++++ 13 files changed, 782 insertions(+) create mode 100644 src/features/fee/FeeTransferCompleteScreen.tsx create mode 100644 src/features/fee/FeeTransferConfirmScreen.tsx create mode 100644 src/features/fee/FeeTransferSemesterScreen.tsx create mode 100644 src/features/fee/FeeTransferTossScreen.tsx create mode 100644 src/features/fee/FeeTransferTranscriptScreen.tsx create mode 100644 src/features/fee/components/FeeImagePreviewOverlay.tsx create mode 100644 src/features/fee/components/FeeImageUploadButton.tsx create mode 100644 src/features/fee/components/FeeTranscriptGuide.tsx create mode 100644 src/features/fee/components/FeeTransferStepLayout.tsx create mode 100644 src/features/fee/components/FeeUploadedFileCard.tsx create mode 100644 src/features/fee/constants/fee.ts 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/features/fee/FeeTransferCompleteScreen.tsx b/src/features/fee/FeeTransferCompleteScreen.tsx new file mode 100644 index 0000000..14af947 --- /dev/null +++ b/src/features/fee/FeeTransferCompleteScreen.tsx @@ -0,0 +1,27 @@ +import { useNavigate } from "react-router-dom"; + +import CompleteCheck from "@/components/ui/CompleteCheck"; +import ResultScreen from "@/components/ui/ResultScreen"; + +// Figma: 송금 확인요청 완료 페이지 (nodeId 3562:163013) +// 체크 모션이 신청 완료 화면들과 같은 메인 컴포넌트(Circle Check Motion 1712:192849)의 +// 인스턴스라 ResultScreen + CompleteCheck를 그대로 쓴다. 버튼이 하나뿐이라 secondaryAction은 없다. +function FeeTransferCompleteScreen() { + const navigate = useNavigate(); + const goHome = () => 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..a6a6c97 --- /dev/null +++ b/src/features/fee/FeeTransferConfirmScreen.tsx @@ -0,0 +1,51 @@ +import { ActionAreaButton } from "@wanteddev/wds"; +import { useNavigate } from "react-router-dom"; + +import sendFastIcon from "@/assets/icons/fee/send-fast.svg"; +import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; +import { FEE_TRANSFER_PATHS } from "@/features/fee/constants/fee"; + +// Figma: 계좌 송금 - 토스 다녀와서 확인 버튼 (nodeId 3562:162973) +// 토스에서 송금하고 돌아온 사용자에게 확인을 받는 단계. 실제 입금 확인은 학생회가 수동으로 +// 하기 때문에 여기서 "네"를 눌러야 확인 요청이 접수된다. +// +// 일러스트에 Figma Motion(종이비행기가 튀어 들어오는 2초 루프)이 붙어 있지만, Lottie는 +// Dev mode에서 뽑을 수 없고 LottieFiles 플러그인으로 디자이너가 내보내야 한다 +// (component-convention.md "모션 (Lottie)"). 받기 전까지는 정지 SVG로 둔다 — +// 키프레임을 손으로 옮기지 않는다. +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..b5336a4 --- /dev/null +++ b/src/features/fee/FeeTransferTossScreen.tsx @@ -0,0 +1,87 @@ +import { ActionAreaButton } from "@wanteddev/wds"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import wonIcon from "@/assets/icons/fee/won.svg"; +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 결과로 판정한다. +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/FeeTranscriptGuide.tsx b/src/features/fee/components/FeeTranscriptGuide.tsx new file mode 100644 index 0000000..4d7cce0 --- /dev/null +++ b/src/features/fee/components/FeeTranscriptGuide.tsx @@ -0,0 +1,30 @@ +import { Typography } from "@wanteddev/wds"; + +import transcriptExample from "@/assets/images/fee/transcript-example.png"; + +// Figma: Transcript Preview Section (nodeId 3562:162857) +// 어떤 화면을 캡처해야 하는지 보여주는 예시 카드. 안쪽 이미지는 가려진 개인정보까지 포함해 +// 디자인에서 그린 그림이라 Figma export를 그대로 쓴다(실제 사용자 데이터가 아니다). +function FeeTranscriptGuide() { + return ( + // Figma 카드는 248px에서 잘린다 — 예시 이미지 아래쪽(이수학기 행)이 일부러 잘려 보인다 +
+ + 캡처 예시 + + 학점이수현황 캡처 예시 — 학생정보와 이수학기가 보이는 화면 +
+ ); +} + +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"; From d9fbf5b35e00360c90ff0b0d56cf8ccf03d12948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:52:19 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=EC=BA=A1=EC=B2=98=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20=EA=B3=A0?= =?UTF-8?q?=ED=95=B4=EC=83=81=EB=8F=84=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=ED=83=80=EC=9D=B4=ED=8F=AC=20variant=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 예시 이미지를 1배(280x205)로 받아 고해상도 화면에서 흐리게 보였다. 3배(840x615)로 다시 받고, 280px 그림이 335px 카드 안에 정확히 들어가도록 좌우 여백을 27.5px로 맞춘다. "캡처 예시" 라벨은 Figma가 Caption 2/Regular(11px)인데 caption1(12px)로 썼었다. 이 그림은 실제 K-스마트멘토 스크린샷 위에 가림 박스와 가짜 값을 얹어 만든 것이라 레이어가 합쳐진 export만 커밋해야 한다. 가림 박스가 전부 불투명해 밑글씨가 비치지 않는 것을 픽셀로 확인했고, 그 전제를 컴포넌트 주석에 남겼다. Co-Authored-By: Claude Opus 5 (1M context) --- src/assets/images/fee/transcript-example.png | Bin 9994 -> 35717 bytes .../fee/components/FeeTranscriptGuide.tsx | 20 ++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/assets/images/fee/transcript-example.png b/src/assets/images/fee/transcript-example.png index 5ffff003fdc803bd5288fe2a97e0ea6e8f8587f7..ebf6209d75f31dbf4d1aa51878e81e99b06a2d6a 100644 GIT binary patch literal 35717 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 RtW*t7PD)9#QrtNBKLAQ@0!#n^ diff --git a/src/features/fee/components/FeeTranscriptGuide.tsx b/src/features/fee/components/FeeTranscriptGuide.tsx index 4d7cce0..7e9a5ca 100644 --- a/src/features/fee/components/FeeTranscriptGuide.tsx +++ b/src/features/fee/components/FeeTranscriptGuide.tsx @@ -3,25 +3,33 @@ import { Typography } from "@wanteddev/wds"; import transcriptExample from "@/assets/images/fee/transcript-example.png"; // Figma: Transcript Preview Section (nodeId 3562:162857) -// 어떤 화면을 캡처해야 하는지 보여주는 예시 카드. 안쪽 이미지는 가려진 개인정보까지 포함해 -// 디자인에서 그린 그림이라 Figma export를 그대로 쓴다(실제 사용자 데이터가 아니다). +// 어떤 화면을 캡처해야 하는지 보여주는 예시 카드. +// +// 안쪽 그림(3562:162858)은 Figma에서 실제 K-스마트멘토 스크린샷 위에 가림 박스 9개와 가짜 값 +// (소프트웨어전공 / 20260000 / 홍길동 등)을 얹어 만든 것이다. 그래서 레이어별로 쪼개진 +// `svgAssets`나 원본 `rawImages`를 쓰면 안 되고, 가림까지 합쳐진 `export` PNG만 커밋한다 — +// 커밋 전에 가림 박스가 전부 불투명한지(밑글씨가 비치지 않는지) 픽셀로 확인했다. +// 3배 크기(840x615)로 받아서 고해상도 화면에서도 또렷하게 보이게 한다. function FeeTranscriptGuide() { return ( // Figma 카드는 248px에서 잘린다 — 예시 이미지 아래쪽(이수학기 행)이 일부러 잘려 보인다 -
+ // 335px 카드 안에 280px 그림을 가운데 두면 좌우가 27.5px씩 남는다(Figma는 28/27로 반올림) +
캡처 예시 학점이수현황 캡처 예시 — 학생정보와 이수학기가 보이는 화면
); From 7a6ccebeda152bed9e7f2ce5515277f4cc4e27a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:59:24 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=ED=86=A0=EC=8A=A4=20=EC=86=A1?= =?UTF-8?q?=EA=B8=88=20=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit supertoss://send 딥링크로 토스를 열고, 앱에 다녀오면 송금 확인 단계로 넘긴다. 브라우저는 커스텀 스킴이 실제로 열렸는지 알려주지 않는다 — window.open은 앱이 없어도 창 객체를 돌려준다. 그래서 "앱으로 전환되면 탭이 가려진다"는 성질을 쓴다: 1.5초 안에 화면이 가려지면 성공으로 보고 복귀를 기다리고, 가려지지 않은 채 타이머가 울리면 이동 실패 화면을 띄운다. 입금 계좌는 저장소가 public이라 코드에 넣지 않고 VITE_FEE_TOSS_* 환경 변수로 받는다(.env는 .gitignore에 있다). 값이 없으면 빈 URL이라 곧바로 실패 화면이 된다. 금액은 남은 학기 수별 규칙을 아직 못 받아서 목업 상수로 보낸다. Co-Authored-By: Claude Opus 5 (1M context) --- src/features/fee/FeeTransferTossScreen.tsx | 38 ++++---- .../fee/FeeTransferTranscriptScreen.tsx | 6 +- src/features/fee/constants/fee.ts | 42 ++++++--- src/features/fee/hooks/useTossDeepLink.ts | 90 +++++++++++++++++++ src/vite-env.d.ts | 14 +++ 5 files changed, 156 insertions(+), 34 deletions(-) create mode 100644 src/features/fee/hooks/useTossDeepLink.ts create mode 100644 src/vite-env.d.ts diff --git a/src/features/fee/FeeTransferTossScreen.tsx b/src/features/fee/FeeTransferTossScreen.tsx index b5336a4..29d331e 100644 --- a/src/features/fee/FeeTransferTossScreen.tsx +++ b/src/features/fee/FeeTransferTossScreen.tsx @@ -1,42 +1,38 @@ import { ActionAreaButton } from "@wanteddev/wds"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useNavigate } from "react-router-dom"; import wonIcon from "@/assets/icons/fee/won.svg"; import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; import { - FEE_EXTERNAL_LINKS, + buildTossTransferUrl, + FEE_MOCK_AMOUNT, FEE_TRANSFER_PATHS, } from "@/features/fee/constants/fee"; +import { useTossDeepLink } from "@/features/fee/hooks/useTossDeepLink"; // Figma: 계좌 송금 - 토스로 이동 안내 (nodeId 3562:163025) / 토스 이동 실패 (3562:163046) // // 두 프레임은 같은 화면의 상태 차이다 — 실패하면 일러스트가 빠지고 문구와 버튼 라벨만 바뀐다. // 그래서 라우트를 나누지 않고 실패 여부 state로 토글한다. // -// 토스는 딥링크라 열렸는지 브라우저가 알려주지 않는다. 링크 주소 자체가 아직 없어서 지금은 -// 상수가 비어 있으면 곧바로 실패 상태로 두고, 주소가 생기면 window.open 결과로 판정한다. +// Figma 문구("송금 후 이 화면으로 돌아와 송금 완료 여부를 꼭 확인해 주세요")대로, 토스에 갔다가 +// 브라우저로 돌아오면 송금 확인 단계로 넘긴다. 복귀 감지는 useTossDeepLink이 한다. function FeeTransferTossScreen() { const navigate = useNavigate(); const [hasFailed, setFailed] = useState(false); + const handleFail = useCallback(() => setFailed(true), []); + const handleReturn = useCallback( + () => navigate(FEE_TRANSFER_PATHS.confirm), + [navigate], + ); + const { open } = useTossDeepLink({ + onFail: handleFail, + onReturn: handleReturn, + }); - 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); - }; + // 금액은 남은 학기 수에 따라 달라져야 하는데 규칙을 아직 못 받아서 목업 값으로 보낸다 + const handleOpenToss = () => open(buildTossTransferUrl(FEE_MOCK_AMOUNT)); if (hasFailed) { return ( diff --git a/src/features/fee/FeeTransferTranscriptScreen.tsx b/src/features/fee/FeeTransferTranscriptScreen.tsx index 3ecbdaf..d2ff26b 100644 --- a/src/features/fee/FeeTransferTranscriptScreen.tsx +++ b/src/features/fee/FeeTransferTranscriptScreen.tsx @@ -9,7 +9,7 @@ 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_K_SMART_MENTOR_URL, FEE_TRANSFER_PATHS, } from "@/features/fee/constants/fee"; @@ -40,8 +40,8 @@ function FeeTransferTranscriptScreen() { const handleGuideConfirm = () => { setGuideModalOpen(false); - if (FEE_EXTERNAL_LINKS.kSmartMentor) { - window.open(FEE_EXTERNAL_LINKS.kSmartMentor, "_blank", "noopener"); + if (FEE_K_SMART_MENTOR_URL) { + window.open(FEE_K_SMART_MENTOR_URL, "_blank", "noopener"); } }; diff --git a/src/features/fee/constants/fee.ts b/src/features/fee/constants/fee.ts index 9d3a6a2..6819f1e 100644 --- a/src/features/fee/constants/fee.ts +++ b/src/features/fee/constants/fee.ts @@ -8,16 +8,38 @@ export const FEE_TRANSFER_PATHS = { transcript: "/fee/transfer/transcript", } as const; -// 아직 받지 못한 외부 연동 주소들. 값이 있을 때만 열도록 해서 빈 창이 뜨는 걸 막는다 -// (설정 화면의 SETTINGS_EXTERNAL_LINKS와 같은 방식). -// -// - kSmartMentor: 학점이수현황을 확인하는 국민대 K-스마트멘토 주소 -// - tossTransfer: 계좌·금액이 채워진 토스 송금 딥링크. 학생회 계좌와 납부 금액 규칙이 정해져야 -// 만들 수 있다(남은 학기 수에 따라 금액이 달라진다). -export const FEE_EXTERNAL_LINKS = { - kSmartMentor: "", - tossTransfer: "", -} as const; +// 학점이수현황을 확인하는 국민대 K-스마트멘토 주소. 아직 못 받아서 비워 둔다 — +// 값이 있을 때만 열어서 빈 창이 뜨지 않게 한다. +export const FEE_K_SMART_MENTOR_URL = ""; // 업로드 받는 이미지 형식 — 학점이수현황 "캡처"라 스크린샷 확장자만 받는다 export const FEE_TRANSCRIPT_ACCEPT = "image/png,image/jpeg,image/webp"; + +// 남은 학기 수에 따라 금액이 달라지는 규칙을 아직 못 받았다. 연동 전까지 이 값으로 보낸다. +export const FEE_MOCK_AMOUNT = 10_000; + +// 토스 송금 딥링크(`supertoss://send`)에 넣을 입금 계좌. +// +// 이 저장소는 public이라 계좌번호를 코드에 박지 않는다 — 한 번 커밋하면 지워도 히스토리·포크에 +// 남는다. 테스트할 땐 저장소 루트에 `.env`(이미 .gitignore에 있다)를 만들어 채운다: +// +// VITE_FEE_TOSS_BANK=국민 +// VITE_FEE_TOSS_ACCOUNT_NO=00000000000000 +// +// 학생회 계좌가 확정되면 서버가 내려주도록 바꾸는 게 맞다(금액 규칙도 서버가 알아야 한다). +const TOSS_BANK = import.meta.env.VITE_FEE_TOSS_BANK ?? ""; +const TOSS_ACCOUNT_NO = import.meta.env.VITE_FEE_TOSS_ACCOUNT_NO ?? ""; + +/** 계좌 정보가 없으면 빈 문자열을 돌려준다 — 호출하는 쪽이 이동 실패로 처리한다. */ +export function buildTossTransferUrl(amount: number): string { + if (!TOSS_BANK || !TOSS_ACCOUNT_NO) { + return ""; + } + const params = new URLSearchParams({ + accountNo: TOSS_ACCOUNT_NO, + amount: String(amount), + bank: TOSS_BANK, + }); + + return `supertoss://send?${params.toString()}`; +} diff --git a/src/features/fee/hooks/useTossDeepLink.ts b/src/features/fee/hooks/useTossDeepLink.ts new file mode 100644 index 0000000..04002cf --- /dev/null +++ b/src/features/fee/hooks/useTossDeepLink.ts @@ -0,0 +1,90 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +// 딥링크를 띄운 뒤 이 시간 안에 화면이 가려지지 않으면 "안 열렸다"고 본다. +// 앱 전환은 보통 1초 안에 끝나고, 너무 짧게 잡으면 느린 기기에서 멀쩡히 열린 걸 실패로 센다. +const LAUNCH_TIMEOUT_MS = 1_500; + +type LaunchPhase = "idle" | "launching" | "left"; + +interface UseTossDeepLinkOptions { + /** 앱이 안 열렸다고 판단했을 때 */ + onFail: () => void; + /** 앱에 갔다가 이 화면으로 돌아왔을 때 */ + onReturn: () => void; +} + +/** + * 토스 같은 커스텀 스킴(`supertoss://`)을 열고, 앱에 다녀왔는지를 추정한다. + * + * 브라우저는 커스텀 스킴이 실제로 열렸는지 알려주지 않는다 — `window.open`은 앱이 없어도 + * 창 객체를 돌려주고, `location.href` 대입은 아무 신호도 남기지 않는다. 그래서 표준적인 + * 방법대로 "앱으로 전환되면 이 탭이 가려진다"는 성질을 쓴다. + * + * 열기 → 타이머 시작 + * ├ 1.5초 안에 화면이 가려짐 → 앱으로 갔다("left"). 다시 보이면 onReturn + * └ 가려지지 않은 채 타이머 울림 → 안 열렸다. onFail + * + * 데스크톱 브라우저에서는 전환이 일어나지 않아 항상 실패로 떨어진다 — 실패 화면을 확인하기엔 + * 오히려 편하다. + */ +export function useTossDeepLink({ onFail, onReturn }: UseTossDeepLinkOptions) { + const [phase, setPhase] = useState("idle"); + const timerRef = useRef(null); + + const clearTimer = useCallback(() => { + if (timerRef.current !== null) { + window.clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + + useEffect(() => { + if (phase === "idle") { + return; + } + + const handleVisibility = () => { + if (phase === "launching" && document.visibilityState === "hidden") { + // 앱으로 전환됐다 — 실패 판정을 취소하고 복귀를 기다린다 + clearTimer(); + setPhase("left"); + return; + } + if (phase === "left" && document.visibilityState === "visible") { + setPhase("idle"); + onReturn(); + } + }; + document.addEventListener("visibilitychange", handleVisibility); + // iOS Safari는 앱 전환 때 visibilitychange 대신 pagehide만 주는 경우가 있다 + window.addEventListener("pagehide", handleVisibility); + + return () => { + document.removeEventListener("visibilitychange", handleVisibility); + window.removeEventListener("pagehide", handleVisibility); + }; + }, [phase, clearTimer, onReturn]); + + // 화면을 떠날 때 타이머가 남아 있으면 엉뚱한 시점에 실패 처리가 돈다 + useEffect(() => clearTimer, [clearTimer]); + + const open = useCallback( + (url: string) => { + if (!url) { + onFail(); + return; + } + setPhase("launching"); + // 커스텀 스킴은 window.open보다 location 대입이 막히는 경우가 적다 + window.location.href = url; + timerRef.current = window.setTimeout(() => { + timerRef.current = null; + setPhase("idle"); + onFail(); + }, LAUNCH_TIMEOUT_MS); + }, + [onFail], + ); + + return { open }; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..9df0e4b --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,14 @@ +/// + +// `vite/client`의 ImportMetaEnv는 인덱스 시그니처라 오타가 타입 검사에 안 걸린다. +// 우리가 쓰는 환경 변수만 여기에 적어서 이름을 고정한다. +interface ImportMetaEnv { + /** 토스 송금 딥링크에 넣을 은행명(예: 국민). 계좌 정보라 저장소에 커밋하지 않는다. */ + readonly VITE_FEE_TOSS_BANK?: string; + /** 토스 송금 딥링크에 넣을 계좌번호(하이픈 없이). 계좌 정보라 저장소에 커밋하지 않는다. */ + readonly VITE_FEE_TOSS_ACCOUNT_NO?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} From d4c6db270fb065be980eb6b44da8f37b6b950158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:03:17 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Revert=20"feat:=20=ED=86=A0=EC=8A=A4=20?= =?UTF-8?q?=EC=86=A1=EA=B8=88=20=EB=94=A5=EB=A7=81=ED=81=AC=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실기기에서만 동작을 확인할 수 있어 검증이 안 된 채로 들어가는 걸 피한다. 토스 화면은 다시 "누르면 바로 이동 실패"로 돌아간다. This reverts commit 7a6cceb. --- src/features/fee/FeeTransferTossScreen.tsx | 38 ++++---- .../fee/FeeTransferTranscriptScreen.tsx | 6 +- src/features/fee/constants/fee.ts | 42 +++------ src/features/fee/hooks/useTossDeepLink.ts | 90 ------------------- src/vite-env.d.ts | 14 --- 5 files changed, 34 insertions(+), 156 deletions(-) delete mode 100644 src/features/fee/hooks/useTossDeepLink.ts delete mode 100644 src/vite-env.d.ts diff --git a/src/features/fee/FeeTransferTossScreen.tsx b/src/features/fee/FeeTransferTossScreen.tsx index 29d331e..b5336a4 100644 --- a/src/features/fee/FeeTransferTossScreen.tsx +++ b/src/features/fee/FeeTransferTossScreen.tsx @@ -1,38 +1,42 @@ import { ActionAreaButton } from "@wanteddev/wds"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { useNavigate } from "react-router-dom"; import wonIcon from "@/assets/icons/fee/won.svg"; import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; import { - buildTossTransferUrl, - FEE_MOCK_AMOUNT, + FEE_EXTERNAL_LINKS, FEE_TRANSFER_PATHS, } from "@/features/fee/constants/fee"; -import { useTossDeepLink } from "@/features/fee/hooks/useTossDeepLink"; // Figma: 계좌 송금 - 토스로 이동 안내 (nodeId 3562:163025) / 토스 이동 실패 (3562:163046) // // 두 프레임은 같은 화면의 상태 차이다 — 실패하면 일러스트가 빠지고 문구와 버튼 라벨만 바뀐다. // 그래서 라우트를 나누지 않고 실패 여부 state로 토글한다. // -// Figma 문구("송금 후 이 화면으로 돌아와 송금 완료 여부를 꼭 확인해 주세요")대로, 토스에 갔다가 -// 브라우저로 돌아오면 송금 확인 단계로 넘긴다. 복귀 감지는 useTossDeepLink이 한다. +// 토스는 딥링크라 열렸는지 브라우저가 알려주지 않는다. 링크 주소 자체가 아직 없어서 지금은 +// 상수가 비어 있으면 곧바로 실패 상태로 두고, 주소가 생기면 window.open 결과로 판정한다. function FeeTransferTossScreen() { const navigate = useNavigate(); const [hasFailed, setFailed] = useState(false); - const handleFail = useCallback(() => setFailed(true), []); - const handleReturn = useCallback( - () => navigate(FEE_TRANSFER_PATHS.confirm), - [navigate], - ); - const { open } = useTossDeepLink({ - onFail: handleFail, - onReturn: handleReturn, - }); - // 금액은 남은 학기 수에 따라 달라져야 하는데 규칙을 아직 못 받아서 목업 값으로 보낸다 - const handleOpenToss = () => open(buildTossTransferUrl(FEE_MOCK_AMOUNT)); + 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 ( diff --git a/src/features/fee/FeeTransferTranscriptScreen.tsx b/src/features/fee/FeeTransferTranscriptScreen.tsx index d2ff26b..3ecbdaf 100644 --- a/src/features/fee/FeeTransferTranscriptScreen.tsx +++ b/src/features/fee/FeeTransferTranscriptScreen.tsx @@ -9,7 +9,7 @@ import FeeTranscriptGuide from "@/features/fee/components/FeeTranscriptGuide"; import FeeTransferStepLayout from "@/features/fee/components/FeeTransferStepLayout"; import FeeUploadedFileCard from "@/features/fee/components/FeeUploadedFileCard"; import { - FEE_K_SMART_MENTOR_URL, + FEE_EXTERNAL_LINKS, FEE_TRANSFER_PATHS, } from "@/features/fee/constants/fee"; @@ -40,8 +40,8 @@ function FeeTransferTranscriptScreen() { const handleGuideConfirm = () => { setGuideModalOpen(false); - if (FEE_K_SMART_MENTOR_URL) { - window.open(FEE_K_SMART_MENTOR_URL, "_blank", "noopener"); + if (FEE_EXTERNAL_LINKS.kSmartMentor) { + window.open(FEE_EXTERNAL_LINKS.kSmartMentor, "_blank", "noopener"); } }; diff --git a/src/features/fee/constants/fee.ts b/src/features/fee/constants/fee.ts index 6819f1e..9d3a6a2 100644 --- a/src/features/fee/constants/fee.ts +++ b/src/features/fee/constants/fee.ts @@ -8,38 +8,16 @@ export const FEE_TRANSFER_PATHS = { transcript: "/fee/transfer/transcript", } as const; -// 학점이수현황을 확인하는 국민대 K-스마트멘토 주소. 아직 못 받아서 비워 둔다 — -// 값이 있을 때만 열어서 빈 창이 뜨지 않게 한다. -export const FEE_K_SMART_MENTOR_URL = ""; +// 아직 받지 못한 외부 연동 주소들. 값이 있을 때만 열도록 해서 빈 창이 뜨는 걸 막는다 +// (설정 화면의 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"; - -// 남은 학기 수에 따라 금액이 달라지는 규칙을 아직 못 받았다. 연동 전까지 이 값으로 보낸다. -export const FEE_MOCK_AMOUNT = 10_000; - -// 토스 송금 딥링크(`supertoss://send`)에 넣을 입금 계좌. -// -// 이 저장소는 public이라 계좌번호를 코드에 박지 않는다 — 한 번 커밋하면 지워도 히스토리·포크에 -// 남는다. 테스트할 땐 저장소 루트에 `.env`(이미 .gitignore에 있다)를 만들어 채운다: -// -// VITE_FEE_TOSS_BANK=국민 -// VITE_FEE_TOSS_ACCOUNT_NO=00000000000000 -// -// 학생회 계좌가 확정되면 서버가 내려주도록 바꾸는 게 맞다(금액 규칙도 서버가 알아야 한다). -const TOSS_BANK = import.meta.env.VITE_FEE_TOSS_BANK ?? ""; -const TOSS_ACCOUNT_NO = import.meta.env.VITE_FEE_TOSS_ACCOUNT_NO ?? ""; - -/** 계좌 정보가 없으면 빈 문자열을 돌려준다 — 호출하는 쪽이 이동 실패로 처리한다. */ -export function buildTossTransferUrl(amount: number): string { - if (!TOSS_BANK || !TOSS_ACCOUNT_NO) { - return ""; - } - const params = new URLSearchParams({ - accountNo: TOSS_ACCOUNT_NO, - amount: String(amount), - bank: TOSS_BANK, - }); - - return `supertoss://send?${params.toString()}`; -} diff --git a/src/features/fee/hooks/useTossDeepLink.ts b/src/features/fee/hooks/useTossDeepLink.ts deleted file mode 100644 index 04002cf..0000000 --- a/src/features/fee/hooks/useTossDeepLink.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -// 딥링크를 띄운 뒤 이 시간 안에 화면이 가려지지 않으면 "안 열렸다"고 본다. -// 앱 전환은 보통 1초 안에 끝나고, 너무 짧게 잡으면 느린 기기에서 멀쩡히 열린 걸 실패로 센다. -const LAUNCH_TIMEOUT_MS = 1_500; - -type LaunchPhase = "idle" | "launching" | "left"; - -interface UseTossDeepLinkOptions { - /** 앱이 안 열렸다고 판단했을 때 */ - onFail: () => void; - /** 앱에 갔다가 이 화면으로 돌아왔을 때 */ - onReturn: () => void; -} - -/** - * 토스 같은 커스텀 스킴(`supertoss://`)을 열고, 앱에 다녀왔는지를 추정한다. - * - * 브라우저는 커스텀 스킴이 실제로 열렸는지 알려주지 않는다 — `window.open`은 앱이 없어도 - * 창 객체를 돌려주고, `location.href` 대입은 아무 신호도 남기지 않는다. 그래서 표준적인 - * 방법대로 "앱으로 전환되면 이 탭이 가려진다"는 성질을 쓴다. - * - * 열기 → 타이머 시작 - * ├ 1.5초 안에 화면이 가려짐 → 앱으로 갔다("left"). 다시 보이면 onReturn - * └ 가려지지 않은 채 타이머 울림 → 안 열렸다. onFail - * - * 데스크톱 브라우저에서는 전환이 일어나지 않아 항상 실패로 떨어진다 — 실패 화면을 확인하기엔 - * 오히려 편하다. - */ -export function useTossDeepLink({ onFail, onReturn }: UseTossDeepLinkOptions) { - const [phase, setPhase] = useState("idle"); - const timerRef = useRef(null); - - const clearTimer = useCallback(() => { - if (timerRef.current !== null) { - window.clearTimeout(timerRef.current); - timerRef.current = null; - } - }, []); - - useEffect(() => { - if (phase === "idle") { - return; - } - - const handleVisibility = () => { - if (phase === "launching" && document.visibilityState === "hidden") { - // 앱으로 전환됐다 — 실패 판정을 취소하고 복귀를 기다린다 - clearTimer(); - setPhase("left"); - return; - } - if (phase === "left" && document.visibilityState === "visible") { - setPhase("idle"); - onReturn(); - } - }; - document.addEventListener("visibilitychange", handleVisibility); - // iOS Safari는 앱 전환 때 visibilitychange 대신 pagehide만 주는 경우가 있다 - window.addEventListener("pagehide", handleVisibility); - - return () => { - document.removeEventListener("visibilitychange", handleVisibility); - window.removeEventListener("pagehide", handleVisibility); - }; - }, [phase, clearTimer, onReturn]); - - // 화면을 떠날 때 타이머가 남아 있으면 엉뚱한 시점에 실패 처리가 돈다 - useEffect(() => clearTimer, [clearTimer]); - - const open = useCallback( - (url: string) => { - if (!url) { - onFail(); - return; - } - setPhase("launching"); - // 커스텀 스킴은 window.open보다 location 대입이 막히는 경우가 적다 - window.location.href = url; - timerRef.current = window.setTimeout(() => { - timerRef.current = null; - setPhase("idle"); - onFail(); - }, LAUNCH_TIMEOUT_MS); - }, - [onFail], - ); - - return { open }; -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 9df0e4b..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -// `vite/client`의 ImportMetaEnv는 인덱스 시그니처라 오타가 타입 검사에 안 걸린다. -// 우리가 쓰는 환경 변수만 여기에 적어서 이름을 고정한다. -interface ImportMetaEnv { - /** 토스 송금 딥링크에 넣을 은행명(예: 국민). 계좌 정보라 저장소에 커밋하지 않는다. */ - readonly VITE_FEE_TOSS_BANK?: string; - /** 토스 송금 딥링크에 넣을 계좌번호(하이픈 없이). 계좌 정보라 저장소에 커밋하지 않는다. */ - readonly VITE_FEE_TOSS_ACCOUNT_NO?: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} From e637bd38b0fa3de147b95f4f53055ed0134bc0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:13:07 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=EC=BA=A1=EC=B2=98=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EC=B9=B4=EB=93=9C=EA=B0=80=20375px=20=EB=B0=96?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B7=B8=EB=A6=BC=EC=9D=B4=20=EC=9E=98?= =?UTF-8?q?=EB=A6=AC=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 화면 프레임이 w-full / sm:w-[480px]라 카드 폭이 변하는데 카드 높이를 375 기준 248px로 박고 그림을 w-full로 둬서, 폭이 넓어지면 그림이 같이 커져 아래가 잘렸다 (430px에서 40px, 480px 프레임에서 77px). 반대로 좁으면 아래에 빈 공간이 생겼다. 높이를 내용이 정하게 두고 그림은 Figma 크기인 280px로 고정한다 — 카드가 더 좁으면 preflight의 max-width:100%가 줄여준다. 설계 폭에서는 그대로 248px이 나온다. Co-Authored-By: Claude Opus 5 (1M context) --- src/features/fee/components/FeeTranscriptGuide.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/features/fee/components/FeeTranscriptGuide.tsx b/src/features/fee/components/FeeTranscriptGuide.tsx index 7e9a5ca..fdfd281 100644 --- a/src/features/fee/components/FeeTranscriptGuide.tsx +++ b/src/features/fee/components/FeeTranscriptGuide.tsx @@ -13,8 +13,11 @@ import transcriptExample from "@/assets/images/fee/transcript-example.png"; function FeeTranscriptGuide() { return ( // Figma 카드는 248px에서 잘린다 — 예시 이미지 아래쪽(이수학기 행)이 일부러 잘려 보인다 - // 335px 카드 안에 280px 그림을 가운데 두면 좌우가 27.5px씩 남는다(Figma는 28/27로 반올림) -
+ // 카드 높이를 고정하지 않는다 — 화면 프레임이 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을 쓰지 않는다. */} 학점이수현황 캡처 예시 — 학생정보와 이수학기가 보이는 화면 Date: Thu, 8 Oct 2026 17:19:15 +0900 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20=EC=86=A1=EA=B8=88=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=9D=BC=EB=9F=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EB=AF=B8=EA=B5=AC=ED=98=84=20=EB=AA=A8=EC=85=98?= =?UTF-8?q?=EC=9D=84=20=EC=BD=94=EB=93=9C=EC=97=90=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_motion_context를 다섯 프레임 전부 돌려보니 ₩ 아이콘(3562:163025)에도 Figma Motion이 있었다. 송금 확인 화면만 확인하고 넘어가 놓쳤던 것이다. 둘 다 Lottie를 받아야 붙일 수 있어 지금은 정지 SVG다 — 어떤 키프레임이 빠져 있는지 주석에 남겨서 재export 때 무엇을 받아야 하는지 알 수 있게 한다. Co-Authored-By: Claude Opus 5 (1M context) --- src/features/fee/FeeTransferConfirmScreen.tsx | 7 ++++--- src/features/fee/FeeTransferTossScreen.tsx | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/features/fee/FeeTransferConfirmScreen.tsx b/src/features/fee/FeeTransferConfirmScreen.tsx index a6a6c97..dba19c8 100644 --- a/src/features/fee/FeeTransferConfirmScreen.tsx +++ b/src/features/fee/FeeTransferConfirmScreen.tsx @@ -9,9 +9,10 @@ import { FEE_TRANSFER_PATHS } from "@/features/fee/constants/fee"; // 토스에서 송금하고 돌아온 사용자에게 확인을 받는 단계. 실제 입금 확인은 학생회가 수동으로 // 하기 때문에 여기서 "네"를 눌러야 확인 요청이 접수된다. // -// 일러스트에 Figma Motion(종이비행기가 튀어 들어오는 2초 루프)이 붙어 있지만, Lottie는 -// Dev mode에서 뽑을 수 없고 LottieFiles 플러그인으로 디자이너가 내보내야 한다 -// (component-convention.md "모션 (Lottie)"). 받기 전까지는 정지 SVG로 둔다 — +// 일러스트에 Figma Motion이 붙어 있다(3562:163004·163010) — 2초 루프 안에서 종이비행기가 +// 0~0.82초에 걸쳐 기울기·크기·위치가 함께 튀어 들어오고(overshoot), 꼬리 획이 0~0.4초에 +// 페이드인한다. Lottie는 Dev mode에서 못 뽑고 디자이너가 LottieFiles 플러그인으로 내보내야 +// 해서(component-convention.md "모션 (Lottie)") 받기 전까지는 정지 SVG로 둔다 — // 키프레임을 손으로 옮기지 않는다. function FeeTransferConfirmScreen() { const navigate = useNavigate(); diff --git a/src/features/fee/FeeTransferTossScreen.tsx b/src/features/fee/FeeTransferTossScreen.tsx index b5336a4..a0a29b8 100644 --- a/src/features/fee/FeeTransferTossScreen.tsx +++ b/src/features/fee/FeeTransferTossScreen.tsx @@ -16,6 +16,12 @@ import { // // 토스는 딥링크라 열렸는지 브라우저가 알려주지 않는다. 링크 주소 자체가 아직 없어서 지금은 // 상수가 비어 있으면 곧바로 실패 상태로 두고, 주소가 생기면 window.open 결과로 판정한다. +// +// ₩ 아이콘에는 Figma Motion이 붙어 있다(3562:163039·163043) — 2초 루프 안에서 세로 획이 +// 0~1.1초, 가로 획이 0.42~1.1초에 걸쳐 그려진다(ease cubic-bezier(0.25, 0.65, 0.35, 1)). +// Lottie는 Dev mode에서 못 뽑고 디자이너가 LottieFiles 플러그인으로 내보내야 해서 +// (component-convention.md "모션 (Lottie)"), 받기 전까지는 다 그려진 정지 SVG로 둔다. +// 키프레임을 손으로 옮기지 않는다. function FeeTransferTossScreen() { const navigate = useNavigate(); const [hasFailed, setFailed] = useState(false); From 9ef11603e34c1ffb48b723d73f14acdfe34793bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= <104981505+xeoxxn@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:02:51 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=EC=86=A1=EA=B8=88=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=9D=BC=EB=9F=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20Lottie=20=EB=AA=A8=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디자이너가 LottieFiles 플러그인으로 내보낸 json을 받아 정지 SVG를 대체한다. 둘 다 48x48 / 60fps / 120프레임(2초)이고 텍스트 레이어는 없다. 재생기는 두 아이콘이 공유한다(FeeLottieIcon). 타임라인은 2초지만 실제 움직임은 앞부분에서 끝나서, 모션 줄이기일 때 세울 프레임을 json에서 직접 확인해 넘긴다 — ₩는 키프레임이 66프레임(1.1초)에서 끝나고, 종이비행기는 플러그인이 이징을 매 프레임으로 구워놔서 값이 실제로 고정되는 49프레임(0.82초)을 찾아야 했다. 브라우저에서 재생도 확인했다. ₩는 path d가 11단계로 변하며 획이 그려지고, 종이비행기는 scale 0.816 → 1.024(overshoot) → 1.0으로 잦아들며 770ms에 멈춘다. Co-Authored-By: Claude Opus 5 (1M context) --- src/assets/icons/fee/send-fast.svg | 9 ---- src/assets/icons/fee/won.svg | 11 ----- src/assets/lottie/fee/send-fast.json | 1 + src/assets/lottie/fee/won.json | 1 + src/features/fee/FeeTransferConfirmScreen.tsx | 15 ++++--- src/features/fee/FeeTransferTossScreen.tsx | 13 +++--- src/features/fee/components/FeeLottieIcon.tsx | 44 +++++++++++++++++++ 7 files changed, 60 insertions(+), 34 deletions(-) delete mode 100644 src/assets/icons/fee/send-fast.svg delete mode 100644 src/assets/icons/fee/won.svg create mode 100644 src/assets/lottie/fee/send-fast.json create mode 100644 src/assets/lottie/fee/won.json create mode 100644 src/features/fee/components/FeeLottieIcon.tsx diff --git a/src/assets/icons/fee/send-fast.svg b/src/assets/icons/fee/send-fast.svg deleted file mode 100644 index 95fa6f3..0000000 --- a/src/assets/icons/fee/send-fast.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/src/assets/icons/fee/won.svg b/src/assets/icons/fee/won.svg deleted file mode 100644 index 5de700c..0000000 --- a/src/assets/icons/fee/won.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - 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/features/fee/FeeTransferConfirmScreen.tsx b/src/features/fee/FeeTransferConfirmScreen.tsx index dba19c8..ef40df2 100644 --- a/src/features/fee/FeeTransferConfirmScreen.tsx +++ b/src/features/fee/FeeTransferConfirmScreen.tsx @@ -1,7 +1,8 @@ import { ActionAreaButton } from "@wanteddev/wds"; import { useNavigate } from "react-router-dom"; -import sendFastIcon from "@/assets/icons/fee/send-fast.svg"; +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"; @@ -9,11 +10,9 @@ import { FEE_TRANSFER_PATHS } from "@/features/fee/constants/fee"; // 토스에서 송금하고 돌아온 사용자에게 확인을 받는 단계. 실제 입금 확인은 학생회가 수동으로 // 하기 때문에 여기서 "네"를 눌러야 확인 요청이 접수된다. // -// 일러스트에 Figma Motion이 붙어 있다(3562:163004·163010) — 2초 루프 안에서 종이비행기가 -// 0~0.82초에 걸쳐 기울기·크기·위치가 함께 튀어 들어오고(overshoot), 꼬리 획이 0~0.4초에 -// 페이드인한다. Lottie는 Dev mode에서 못 뽑고 디자이너가 LottieFiles 플러그인으로 내보내야 -// 해서(component-convention.md "모션 (Lottie)") 받기 전까지는 정지 SVG로 둔다 — -// 키프레임을 손으로 옮기지 않는다. +// 종이비행기(3562:162980)는 기울기·크기·위치가 함께 튀어 들어오는(overshoot) Figma Motion이라 +// Lottie로 받아 재생한다. 플러그인이 이징을 매 프레임으로 구워서 키프레임만 보면 끝을 알 수 +// 없는데, 실제 값은 49프레임(0.82초)부터 고정이다 — rotation 6° → -2.5° → 1° → 0°로 잦아든다. function FeeTransferConfirmScreen() { const navigate = useNavigate(); @@ -43,7 +42,9 @@ function FeeTransferConfirmScreen() { } actionsVariant="strong" contentGap={12} - illustration={} + illustration={ + + } title="송금을 완료하셨나요?" /> ); diff --git a/src/features/fee/FeeTransferTossScreen.tsx b/src/features/fee/FeeTransferTossScreen.tsx index a0a29b8..60284fe 100644 --- a/src/features/fee/FeeTransferTossScreen.tsx +++ b/src/features/fee/FeeTransferTossScreen.tsx @@ -2,7 +2,8 @@ import { ActionAreaButton } from "@wanteddev/wds"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import wonIcon from "@/assets/icons/fee/won.svg"; +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, @@ -17,11 +18,9 @@ import { // 토스는 딥링크라 열렸는지 브라우저가 알려주지 않는다. 링크 주소 자체가 아직 없어서 지금은 // 상수가 비어 있으면 곧바로 실패 상태로 두고, 주소가 생기면 window.open 결과로 판정한다. // -// ₩ 아이콘에는 Figma Motion이 붙어 있다(3562:163039·163043) — 2초 루프 안에서 세로 획이 -// 0~1.1초, 가로 획이 0.42~1.1초에 걸쳐 그려진다(ease cubic-bezier(0.25, 0.65, 0.35, 1)). -// Lottie는 Dev mode에서 못 뽑고 디자이너가 LottieFiles 플러그인으로 내보내야 해서 -// (component-convention.md "모션 (Lottie)"), 받기 전까지는 다 그려진 정지 SVG로 둔다. -// 키프레임을 손으로 옮기지 않는다. +// ₩ 아이콘(3562:163032)은 획이 그려지는 Figma Motion이라 Lottie로 받아 재생한다. 2초 +// 타임라인이지만 실제 움직임은 66프레임(1.1초)에 끝나고 나머지는 멈춰 있는 구간이다 — +// json의 키프레임이 실제로 66에서 끝나는 걸 확인했다. function FeeTransferTossScreen() { const navigate = useNavigate(); const [hasFailed, setFailed] = useState(false); @@ -78,7 +77,7 @@ function FeeTransferTossScreen() { description={ "송금 후 이 화면으로 돌아와\n송금 완료 여부를 꼭 확인해 주세요." } - illustration={} + illustration={} title={ <> 이제 토스에서 학생회비를 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;