Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/conventions/wds-component-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ WDS에 이름이 비슷한 `ProgressIndicator`가 실제로 존재해서(`제외

| 블록 | Figma | 코드 |
|---|---|---|
| `lockerGroup` | Zone Area + Locker Grid | `LockersLockerGrid`(`bordered`면 `Line/Solid/Alternative` 테두리 상자) |
| `lockerGroup` | Zone Area + Locker Grid | `LockersGrid`(`bordered`면 `Line/Solid/Alternative` 테두리 상자) |
| `label` | Direction Label·Aisle | `LockersMapLabel` — 세로면 한 글자씩 줄바꿈하고 단어 사이에 빈 줄 |
| `area` | Room Label·Zone Label·Stairs Area | `LockersMapArea` — 구역 선택 평면도와 같은 점선 상자 |
| `shelfLabel` | Shelf Label | `LockersShelfLabel` |
Expand Down Expand Up @@ -609,3 +609,18 @@ Figma는 헤더 배경이 없어서 배너 그림자(`Shadow/Spread/Small`, 60px
### 빈 상태는 WDS `FallbackView`가 아니라 홈 로컬 `HomeEmptyState`

일러스트 44px + 문구 한 줄 + 작은 버튼이라 행사 목록 Empty State 때와 같은 이유(`FallbackView`의 일러스트 폭·패딩 고정)로 맞지 않는다. 그림은 빌릴게 반납 빈 상태(`icons/bililge-empty/return-items.svg`)·행사 목록 빈 상태(`icons/events/empty-events.svg`)와 같은 도형이라 크기만 줄여 그대로 쓴다.

## 내 내역 화면(열린피드백 작성내역 `3147:147748`·`3147:147818`·`3147:147756`·`3147:147779` / 학생회비 납부내역 `3147:147860`·`3147:147879`·`3147:147841` / 사물함 배정 상태 `3147:148357`·`3147:148331`) 구현 중 확정된 매핑

| Figma | 코드 | 확인 내용 |
|---|---|---|
| `Content Badge` (답변완료·답변대기, 납부확인중·확인필요·납부완료, 배정완료) | `ContentBadge size="small" variant="solid" color="accent"` | 홈 내 정보 목록과 같은 상태 이름·색이다. 초록 green, 주황 orange, 빨강 red |
| 작성내역 상세 `해당 회차 답변 모아보기` | `Button color="primary" size="medium" variant="outlined"` | |
| 학생회비 `문의하기`, 사물함 `사물함 위치 보기` | `Button color="assistive" size="medium" variant="outlined" fullWidth` | 위치 보기의 꺾쇠는 `trailingContent={<IconChevronRight />}` |
| 빈 상태 버튼 `열린피드백 작성하기` / `열린피드백 보러 가기` | `Button color="assistive" size="small" variant="solid"` | 행사 목록 빈 상태(outlined)와 variant가 다르다 |
| 뒤로가기 헤더 | `ScreenHeader variant="normal"` + `TopNavigationButton` | 바로 들어와 뒤로 갈 기록이 없으면 홈(상세는 목록, 위치 보기는 배정 상태)으로 보낸다 |

- **카드·빈 상태는 Stream 로컬**: 작성내역 카드(`FeedbacksHistoryCard`), 납부 카드(`FeePaymentCard`), 배정 카드는 흰 바탕 `rounded-xl p-4` 로컬 카드다. 빈 상태는 행사 목록 Empty State와 같은 이유(`FallbackView`의 일러스트 크기·패딩 고정)로 로컬이다. 열린피드백은 `FeedbacksEmptyState`(그림 `icons/feedbacks/empty-feedback.svg`)를 쓰고, 사물함은 행사 목록과 같은 도형이라 `icons/events/empty-events.svg`를 그대로 쓴다.
- **답변 강조 표시는 두 상세 화면이 공유한다**: 답변 글의 `**굵게**`를 볼드 `Typography`로 바꾸는 `FeedbacksAnswerText`를 모아보기 상세와 작성내역 상세가 같이 쓴다.
- **사물함 위치 보기(`/my/locker/location`)는 Figma에 없다**: 칸 선택 화면 배치를 보기 전용으로 쓴다. 칸 영역(미니맵·범례·확대 스크롤)은 `LockersMapView`로 두 화면이 공유한다. 보기 전용에서는 칸을 버튼으로 그리지 않고, 내 칸만 선택된 칸 모양(Primary/Normal 배경 + 흰 번호)으로 표시한다. 범례도 "내 사물함" 하나로 바꾸고, 처음 열 때 내 칸이 화면 밖이면 보일 만큼만 스크롤한다. 신청 화면의 내 칸(초록 테두리, 선택 불가)은 그대로다.
- **Figma와 다르게 둔 값**: 요일은 날짜로 계산해서 Figma의 요일 표기와 다를 수 있다. 사물함 번호는 칸 배치 목데이터가 있는 A-1 구역에 맞춰 A-7로 두었다(Figma는 B-25). 사물함 빈 상태 문구의 연도는 올해다.
45 changes: 43 additions & 2 deletions src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ 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 FeePaymentsScreen from "@/features/fee/FeePaymentsScreen";
import FeedbacksDetailScreen from "@/features/feedbacks/FeedbacksDetailScreen";
import FeedbacksListScreen from "@/features/feedbacks/FeedbacksListScreen";
import FeedbacksMineDetailScreen from "@/features/feedbacks/FeedbacksMineDetailScreen";
import FeedbacksMineScreen from "@/features/feedbacks/FeedbacksMineScreen";
import FeedbacksNewScreen from "@/features/feedbacks/FeedbacksNewScreen";
import HomeScreen from "@/features/home/HomeScreen";
import LockersApplyCompleteScreen from "@/features/lockers/LockersApplyCompleteScreen";
import LockersApplyFailureScreen from "@/features/lockers/LockersApplyFailureScreen";
import LockersApplyScreen from "@/features/lockers/LockersApplyScreen";
import LockersLockerSelectScreen from "@/features/lockers/LockersLockerSelectScreen";
import LockersAssignmentScreen from "@/features/lockers/LockersAssignmentScreen";
import LockersCellSelectScreen from "@/features/lockers/LockersCellSelectScreen";
import LockersMineScreen from "@/features/lockers/LockersMineScreen";
import LockersSectionSelectScreen from "@/features/lockers/LockersSectionSelectScreen";
import NoticesDetailScreen from "@/features/notices/NoticesDetailScreen";
import NoticesListScreen from "@/features/notices/NoticesListScreen";
Expand Down Expand Up @@ -77,6 +82,42 @@ const routes = [
} satisfies ScreenRouteHandle,
path: "/feedbacks/:feedbackId",
},
{
element: <FeedbacksMineScreen />,
// 내 내역 화면 — 하단 탭 없이 회색 배경(기본값)
handle: { hasBottomNav: false } satisfies ScreenRouteHandle,
path: "/my/feedbacks",
},
{
element: <FeedbacksMineDetailScreen />,
// 작성내역 상세 — 열린피드백 상세와 같은 흰 배경, 하단 탭 없음
handle: {
background: "normal",
hasBottomNav: false,
} satisfies ScreenRouteHandle,
path: "/my/feedbacks/:feedbackId",
},
{
element: <FeePaymentsScreen />,
// 내 내역 화면 — 하단 탭 없이 회색 배경(기본값)
handle: { hasBottomNav: false } satisfies ScreenRouteHandle,
path: "/my/fee",
},
{
element: <LockersAssignmentScreen />,
// 내 내역 화면 — 하단 탭 없이 회색 배경(기본값)
handle: { hasBottomNav: false } satisfies ScreenRouteHandle,
path: "/my/locker",
},
{
element: <LockersMineScreen />,
// 칸 선택 화면과 같은 흰 배경
handle: {
background: "normal",
hasBottomNav: false,
} satisfies ScreenRouteHandle,
path: "/my/locker/location",
},
{
element: <EventsDetailScreen />,
// Bottom Nav 대신 하단 고정 버튼(Action Area)이 있는 화면
Expand Down Expand Up @@ -153,7 +194,7 @@ const routes = [
path: "/lockers/apply/sections",
},
{
element: <LockersLockerSelectScreen />,
element: <LockersCellSelectScreen />,
// 구역 선택 화면과 같다 — 하단 고정 선택 영역이 있고, 칸 배경이 흰 면 위에 놓인다
handle: {
background: "normal",
Expand Down
13 changes: 13 additions & 0 deletions src/assets/icons/feedbacks/empty-feedback.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/entities/fee/feeApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FEE_PAYMENTS_MOCK } from "@/entities/fee/feeMock";
import type { FeePayment } from "@/entities/fee/types";
import { mockupApi } from "@/lib/mockupApi";

// 실 API가 붙으면 함수 안쪽만 요청 코드로 바꾼다 — 화면은 이 함수로만 데이터를 받는다.
export function fetchFeePayments(): Promise<FeePayment[]> {
return mockupApi("fee/payments", () => FEE_PAYMENTS_MOCK);
}
57 changes: 57 additions & 0 deletions src/entities/fee/feeMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { FeePayment } from "@/entities/fee/types";

const TITLE = "소프트웨어융합대학 학생회비";

// Figma: 학생회비 납부내역 납부확인중 (nodeId 3147:147860), 확인필요 (3147:147879), 납부완료 (3147:147841)
const CHECKING: FeePayment = {
amount: 150000,
confirmedAt: null,
id: "2026-2",
paidAt: "2026-09-04T11:49",
status: "checking",
title: TITLE,
};

const NEEDS_CHECK: FeePayment = {
amount: null,
confirmedAt: null,
id: "2026-2",
paidAt: "2026-09-04T11:49",
reviewNote:
"입금자 정보를 확인하지 못했어요. 문제가 있을 경우 학생회에 문의해 주세요.",
status: "needs-check",
title: TITLE,
};

const PAID: FeePayment = {
amount: 150000,
confirmedAt: "2026-09-05T13:30",
id: "2026-2",
paidAt: "2026-09-04T11:49",
status: "paid",
title: TITLE,
};

// 여러 학기 내역이 쌓인 경우 — 최신순
const MULTIPLE: FeePayment[] = [
NEEDS_CHECK,
{
...PAID,
confirmedAt: "2026-03-05T13:30",
id: "2026-1",
paidAt: "2026-03-04T11:49",
},
];

// 상태를 바꿔 납부내역 화면을 확인한다
type FeeMockState = "checking" | "needs-check" | "paid" | "multiple";
const FEE_MOCK_STATE: FeeMockState = "multiple";

const FEE_PAYMENTS: Record<FeeMockState, FeePayment[]> = {
checking: [CHECKING],
multiple: MULTIPLE,
"needs-check": [NEEDS_CHECK],
paid: [PAID],
};

export const FEE_PAYMENTS_MOCK = FEE_PAYMENTS[FEE_MOCK_STATE];
14 changes: 14 additions & 0 deletions src/entities/fee/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 납부확인중 · 확인필요 · 납부완료 — 미납이면 내역이 없다(홈에서 납부하기로 안내)
export type FeePaymentStatus = "checking" | "needs-check" | "paid";

export interface FeePayment {
id: string;
status: FeePaymentStatus;
title: string;
// 확인 전이거나 입금자를 확인하지 못하면 금액·확인일시가 비어 있다
amount: number | null;
paidAt: string;
confirmedAt: string | null;
// 확인필요일 때 학생회가 남긴 확인 내용
reviewNote?: string;
}
18 changes: 18 additions & 0 deletions src/entities/feedbacks/feedbacksApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MY_FEEDBACKS_MOCK } from "@/entities/feedbacks/feedbacksMock";
import type { MyFeedback } from "@/entities/feedbacks/types";
import { mockupApi } from "@/lib/mockupApi";

// 실 API가 붙으면 함수 안쪽만 요청 코드로 바꾼다 — 화면은 이 함수로만 데이터를 받는다.
export function fetchMyFeedbacks(): Promise<MyFeedback[]> {
return mockupApi("feedbacks/mine", () => MY_FEEDBACKS_MOCK);
}

// 없는 피드백은 null — 상세 화면이 홈으로 보낸다.
export function fetchMyFeedback(
feedbackId: string,
): Promise<MyFeedback | null> {
return mockupApi(
`feedbacks/mine/${feedbackId}`,
() => MY_FEEDBACKS_MOCK.find((item) => item.id === feedbackId) ?? null,
);
}
74 changes: 74 additions & 0 deletions src/entities/feedbacks/feedbacksMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Feedback, MyFeedback } from "@/entities/feedbacks/types";

// 문단은 빈 줄(\n\n)로 나누고, 굵게 강조할 구간은 **텍스트**로 표시한다 — 둘 다 Figma
// 원본(nodeId 1410:50009)의 문단 구분·볼드 스팬을 그대로 옮긴 것이다.
// `FeedbacksAnswerText`가 이 문법을 해석해서 렌더링하고, 카드 미리보기(FeedbacksQaCard)는
// **만 지운 평문으로 보여준다.
const MT_ANSWER = [
"학우 여러분의 경제적 부담을 덜어드리기 위해 더욱 고민하겠습니다. 최근 **지속적인 물가 상승**으로 인해 숙박 및 대관료가 전반적으로 크게 인상된 상황이었습니다.",
"실제로는 기존보다 참가비가 인상될 수밖에 없는 여건이었으나, 학생회에서는 학우분들의 **부담을 최소화**하기 위해 업체 측과 수차례 협의를 진행하며 최대한 비용을 조정했고, 학교 측 지원금 확보 노력 등을 통해 기존 수준으로 **참가비를 동결**하고자 최선을 다했습니다.",
"그럼에도 불구하고 학우분들께서 체감하시는 부담을 충분히 줄여드리지 못한 점은 학생회 역시 무겁게 받아들이고 있습니다.",
"향후 행사에서는 보다 **합리적이고 만족도 높은 행사 운영**이 이루어질 수 있도록 최선을 다하겠습니다.",
].join("\n\n");

// Figma: 게시판 - 열린피드백 (nodeId 1410:50011) 목업 데이터
export const FEEDBACKS: Feedback[] = [
{
answer: MT_ANSWER,
answerDate: "2026-05-06",
id: "1",
question: "MT 참가 비용이 다소 비싼 편이라 부담스러웠어요.",
round: "2차",
},
{
answer:
"말씀 주신 대로 다음 학기부터는 개강총회를 목요일에 진행해 다음 날 부담을 줄일 수 있도록 반영하겠습니다.",
answerDate: "2026-05-06",
id: "2",
question: "목요일에 개강총회를 진행해서 다음 날 부담이 적어 좋았습니다.",
round: "2차",
},
{
answer: "건의해주신 내용은 다음 학생회 운영위에서 함께 논의하겠습니다.",
answerDate: "2026-03-14",
id: "3",
question: "동아리방 냉난방기 가동 시간을 조금 더 늘려주시면 좋겠습니다.",
round: "1차",
},
{
answer: "안내드린 대로 다음 공지에 상세 시간표를 함께 첨부하겠습니다.",
answerDate: "2026-07-02",
id: "4",
question:
"축제 공연 시간표를 미리 공지해주시면 일정을 맞추기 편할 것 같아요.",
round: "3차",
},
{
answer: "학생증 재발급 신청 절차를 학과 사무실과 다시 공유드리겠습니다.",
answerDate: "2026-08-20",
id: "5",
question: "학생증 재발급 절차가 헷갈려서 안내가 더 필요해요.",
round: "4차",
},
];

// Figma: 열린피드백 작성내역 (nodeId 3147:147748), 상세 (3147:147756 답변완료, 3147:147779 답변대기)
// 답변이 달린 건 열린피드백 목록의 피드백과 같은 id다.
const MY_FEEDBACKS: MyFeedback[] = [
{ ...FEEDBACKS[0], createdAt: "2026-04-25" },
{ ...FEEDBACKS[1], createdAt: "2026-05-02" },
{
createdAt: "2026-04-25",
id: "mine-1",
question:
"다양한 학과 전공 관련 대회나 활동을 통해 포트폴리오를 채워나갈 수 있는 활동이 좋았습니다. 3~5명 정도의 원하는 사람들과 팀을 구성해서 나갈 수 있는 대회나 공모전 정보가 더 많았으면 좋겠어요.",
round: "2차",
},
];

// 상태를 바꿔 작성내역 화면을 확인한다. empty: 빈 상태 (Figma 3147:147818)
type MyFeedbacksMockState = "list" | "empty";
const MY_FEEDBACKS_MOCK_STATE: MyFeedbacksMockState = "list";

export const MY_FEEDBACKS_MOCK =
MY_FEEDBACKS_MOCK_STATE === "list" ? MY_FEEDBACKS : [];
15 changes: 15 additions & 0 deletions src/entities/feedbacks/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type FeedbackRound = "1차" | "2차" | "3차" | "4차";

export interface Feedback {
id: string;
question: string;
round: FeedbackRound;
answer?: string;
// 상세페이지(모아보기)의 "학생회 답변" 날짜. 답변이 없으면 의미가 없어 answer와 함께 옵셔널이다.
answerDate?: string;
}

// 내가 쓴 피드백 — 작성일이 붙는다. 답변이 달리면 같은 id로 열린피드백 목록에도 보인다.
export interface MyFeedback extends Feedback {
createdAt: string;
}
Loading
Loading