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
5 changes: 5 additions & 0 deletions docs/conventions/wds-component-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ WDS 컴포넌트만 합산하면 파일 안에서 **약 350회 이상**의 인
| `Icon/Normal/Home` | Bottom Nav "홈" 탭(Normal 상태) | `980:35475` |
| `Icon/Normal/Ticket` | Bottom Nav "행사" 탭(Normal 상태) | `980:35529` |
| `Icon/Normal/List` | Bottom Nav "게시판" 탭(Normal 상태) | `980:35703` |
| `Tab/Tab` | 게시판-공지 화면(`1256:81776`) 상단 전체/일반 공지/제휴 공지 탭 | `440:7593` — [문서](https://montage.wanted.co.kr/docs/components/navigations/tab/design). 코드 export는 `Tab`(컨텍스트)+`TabList`+`TabListItem` 3개 조합(`TabItem` 같은 단일 export 아님) — `node_modules/@wanteddev/wds/dist/components/tab/`에서 확인 |

### 반례 — 게시판-공지 고정 핀 아이콘은 `Icon/Normal/Pin`(WDS)이 아니었다

`search_design_system`으로 "Icon/Normal/Pin" 이름이 WDS 라이브러리에 있는 걸 확인하고 한 번은 위 표에 WDS로 기록했었다. 그런데 `/figma-check`로 재검증하며 해당 인스턴스(`1256:76326`)의 실제 SVG 에셋을 직접 열어보니, `wds-icon`의 `IconPin`/`IconPinFill`(둘 다 똑바로 선 압정 모양, `currentColor` 상속)과 달리 **기울어진 압정 모양 + `#0066FF` 고정 fill**이 박힌 별개의 도형이었다 — `get_design_context` 응답의 "Component descriptions"에도 이 아이콘 항목이 아예 없었는데(있었다면 처음부터 알아챘을 것) 그때는 놓치고 이름 매칭만 믿었다. **이름이 WDS 컴포넌트와 같아도, `search_design_system` 이름 매칭만으로 확정하지 말고 이 문서의 "완전 확정 방법"(실제 SVG/컴포넌트 설명 대조)까지 거쳐야 한다.** 코드는 실제 Figma SVG를 그대로 받아 `src/assets/icons/pin.svg` + `src/features/notice/components/NoticeCard.tsx` 참고.

코드에서는 `@wanteddev/wds-icon`의 `IconSearch`/`IconBell`/`IconHome`/`IconTicket`/`IconList`로 대응된다(각각 default export를 `index.d.ts`에서 named export로 재노출). `Segmented Control`은 `@wanteddev/wds`의 `SegmentedControl`/`SegmentedControlItem`으로 대응된다.

Expand Down
2 changes: 2 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Route, Routes } from "react-router-dom";

import HomeScreen from "@/features/home/HomeScreen";
import NoticeListScreen from "@/features/notice/NoticeListScreen";
import RentalListScreen from "@/features/rental/RentalListScreen";

// 데스크톱에서 보기 좋게 아이폰 화면 크기로 가운데 정렬만 해준다.
Expand All @@ -9,6 +10,7 @@ function App() {
<div className="flex min-h-screen items-center justify-center bg-[#e5e5e5] py-6">
<Routes>
<Route element={<HomeScreen />} path="/" />
<Route element={<NoticeListScreen />} path="/notice" />
<Route element={<RentalListScreen />} path="/rental" />
</Routes>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/assets/icons/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ function HomeScreen() {
setBottomNavValue(value);
if (value === "rental") {
navigate("/rental");
} else if (value === "board") {
navigate("/notice");
}
};

Expand Down
108 changes: 108 additions & 0 deletions src/features/notice/NoticeListScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
Tab,
TabList,
TabListItem,
TopNavigation,
TopNavigationButton,
} from "@wanteddev/wds";
import { IconBell, IconSearch } from "@wanteddev/wds-icon";
import { Fragment, useState } from "react";
import { useNavigate } from "react-router-dom";

import type { BottomNavValue } from "@/components/ui/BottomNav";
import ScreenLayout from "@/components/ui/ScreenLayout";
import NoticeCard from "@/features/notice/components/NoticeCard";
import {
NOTICES,
type NoticeCategory,
} from "@/features/notice/constants/notices";

type NoticeTab = "all" | "general" | "partnership";

const TAB_CATEGORY: Record<NoticeTab, NoticeCategory | "all"> = {
all: "all",
general: "일반",
partnership: "제휴",
};

// Figma: 게시판 - 공지 (nodeId 1256:81776), 탭 선택 시 (nodeId 1256:81812)
function NoticeListScreen() {
const [tab, setTab] = useState<NoticeTab>("all");
const [bottomNavValue, setBottomNavValue] = useState<BottomNavValue>("board");
const navigate = useNavigate();

const handleBottomNavValueChange = (value: BottomNavValue) => {
setBottomNavValue(value);
if (value === "home") {
navigate("/");
} else if (value === "rental") {
navigate("/rental");
}
};

const notices = NOTICES.filter(
(notice) => tab === "all" || notice.category === TAB_CATEGORY[tab],
);

return (
<ScreenLayout
bottomNavValue={bottomNavValue}
header={
<TopNavigation
background={false}
toolbar={
<Tab
onValueChange={(value) => setTab(value as NoticeTab)}
value={tab}
>
<TabList horizontalPadding>
<TabListItem value="all">전체</TabListItem>
<TabListItem value="general">일반 공지</TabListItem>
<TabListItem value="partnership">제휴 공지</TabListItem>
</TabList>
</Tab>
}
trailingContent={
<>
<TopNavigationButton aria-label="검색" variant="icon">
<IconSearch />
</TopNavigationButton>
<TopNavigationButton aria-label="알림" variant="icon">
<IconBell />
</TopNavigationButton>
</>
}
variant="display"
>
{/* 열린피드백 게시판 전환은 이번 범위 밖이라 비활성 텍스트로만 표시 */}
<span className="flex items-center gap-2">
<span className="text-label-strong">공지</span>
<span className="text-label-disable">열린피드백</span>
</span>
</TopNavigation>
}
onBottomNavValueChange={handleBottomNavValueChange}
>
<div className="flex flex-col items-center gap-4 py-4">
{notices.map((notice, index) => (
<Fragment key={notice.id}>
<NoticeCard
category={notice.category}
date={notice.date}
hasThumbnail={notice.hasThumbnail}
isPinned={notice.isPinned}
title={notice.title}
/>
{index < notices.length - 1 && (
<div className="w-full px-5">
<div className="h-px w-full bg-line-normal-alternative" />
</div>
)}
</Fragment>
))}
</div>
</ScreenLayout>
);
}

export default NoticeListScreen;
50 changes: 50 additions & 0 deletions src/features/notice/components/NoticeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pinIcon from "@/assets/icons/pin.svg";

interface NoticeCardProps {
title: string;
date: string;
category: string;
isPinned?: boolean;
hasThumbnail?: boolean;
}

// Figma: Notice-card (nodeId 1256:81801) — Stream 로컬 컴포넌트, WDS 아님
// (docs/conventions/wds-component-usage.md "제외됨" 표 참고)
// 고정 핀 아이콘은 Icon/Normal/Pin(WDS)로 오판했던 것을 재검증해서 바로잡음 — 실제 Figma
// 에셋은 기울어진 모양 + #0066FF 고정색이라 wds-icon의 IconPin/IconPinFill(직선 모양)과
// 다른 도형이라 대응 export가 없다. src/assets/icons/pin.svg로 원본 SVG를 그대로 받아 씀.
function NoticeCard({
title,
date,
category,
isPinned = false,
hasThumbnail = false,
}: NoticeCardProps) {
return (
<div
className={`flex w-full items-center justify-between px-5 ${hasThumbnail ? "" : "py-[7px]"}`}
>
<div className="flex flex-1 flex-col gap-1">
{hasThumbnail ? (
<div className="flex items-center gap-1">
{isPinned && <img alt="" className="size-5" src={pinIcon} />}
<p className="whitespace-nowrap text-[15px] text-label-normal">
{title}
</p>
</div>
) : (
<p className="text-[15px] text-label-normal">{title}</p>
)}
<p className="text-label-alternative text-xs">
{date} | {category}
</p>
</div>
{/* #d9d9d9는 실제 공지 썸네일 이미지가 아직 없어 Figma가 그대로 쓰는 자리표시 회색이라 시맨틱 토큰 없이 하드코딩 */}
{hasThumbnail && (
<div className="size-14 shrink-0 rounded-lg bg-[#d9d9d9]" />
)}
</div>
);
}

export default NoticeCard;
48 changes: 48 additions & 0 deletions src/features/notice/constants/notices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export type NoticeCategory = "일반" | "제휴";

export interface Notice {
id: string;
title: string;
date: string;
category: NoticeCategory;
isPinned?: boolean;
hasThumbnail?: boolean;
}

// Figma: 게시판 - 공지 (nodeId 1256:81776) 목업 데이터
export const NOTICES: Notice[] = [
{
category: "일반",
date: "2026.09.03",
hasThumbnail: true,
id: "1",
isPinned: true,
title: "2026-2학기 사물함 신청 안내",
},
{
category: "일반",
date: "2026.09.03",
hasThumbnail: true,
id: "2",
title: "2026-2학기 사물함 신청 안내",
},
{
category: "일반",
date: "2026.09.03",
id: "3",
title: "2026-2학기 사물함 신청 안내",
},
{
category: "제휴",
date: "2026.09.03",
id: "4",
title: "2026-2학기 사물함 신청 안내",
},
{
category: "제휴",
date: "2026.09.03",
hasThumbnail: true,
id: "5",
title: "2026-2학기 사물함 신청 안내",
},
];
2 changes: 2 additions & 0 deletions src/features/rental/RentalListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ function RentalListScreen() {
setBottomNavValue(value);
if (value === "home") {
navigate("/");
} else if (value === "board") {
navigate("/notice");
}
};

Expand Down
3 changes: 3 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
/* WDS(@wanteddev/wds/global.css)의 semantic/atomic CSS 변수를 Tailwind 색상 토큰으로 별칭 연결. */
/* 값을 새로 정의하지 않고 WDS 걸 그대로 참조하므로 다크 테마(html[data-theme=dark])도 자동 반영된다. */
/* Figma get_variable_defs로 뽑은 실제 값과 대조해서 확정함(빌릴게 화면, nodeId 1243:73331 기준). */
/* label-disable·line-normal-alternative는 게시판-공지 화면(nodeId 1256:81776) 추가 시 확정. */
--color-label-strong: var(--semantic-label-strong);
--color-label-normal: var(--semantic-label-normal);
--color-label-alternative: var(--semantic-label-alternative);
--color-label-assistive: var(--semantic-label-assistive);
--color-label-disable: var(--semantic-label-disable);

--color-background-normal: var(--semantic-background-normal-normal);
--color-background-alternative: var(--semantic-background-normal-alternative);

--color-line-solid-neutral: var(--semantic-line-solid-neutral);
--color-line-normal-neutral: var(--semantic-line-normal-neutral);
--color-line-normal-alternative: var(--semantic-line-normal-alternative);

--color-primary: var(--semantic-primary-normal);
--color-primary-subtle: var(--atomic-blue-95);
Expand Down
Loading