-
+
+ {/* Figma 그림 자리 81px(size-20.25). 그림(71×70.109)은 SVG 자체 크기로 가운데 둔다 */}
+
+
@@ -44,17 +43,20 @@ function EventsEmptyState({
align="center"
color="semantic.label.alternative"
variant="label1"
- weight="medium"
+ weight={descriptionWeight}
>
{description}
- {actionLabel && (
- // 아카이빙 화면이 아직 없어서 동작은 보류다.
- // TODO: 아카이빙 목록 화면(billilge/stream-client-web#31)이 생기면 onClick을 연결한다
-
- {actionLabel}
+ {action && (
+
+ {action.label}
)}
diff --git a/src/features/events/components/EventsList.tsx b/src/features/events/components/EventsList.tsx
index bbc744c..f9a4df2 100644
--- a/src/features/events/components/EventsList.tsx
+++ b/src/features/events/components/EventsList.tsx
@@ -11,7 +11,7 @@ import EventsEmptyState from "@/features/events/components/EventsEmptyState";
// 버튼은 Figma가 지정한 모집중에만 노출한다 — 모집종료 필터에서 "지난 행사를 보세요"는 모순이 된다.
const EMPTY_STATE_BY_FILTER: Record<
string,
- { title: string; description: string; actionLabel?: string }
+ { title: string; description: string; hasArchivesAction?: boolean }
> = {
all: {
description: "새로운 행사가 열리면 알려드릴게요",
@@ -22,7 +22,7 @@ const EMPTY_STATE_BY_FILTER: Record<
title: "지난 행사가 없어요",
},
open: {
- actionLabel: "아카이빙 둘러보기",
+ hasArchivesAction: true,
description: "지난 행사의 활동을 확인해 보세요",
title: "모집 중인 행사가 없어요",
},
@@ -36,11 +36,17 @@ interface EventsListProps {
statusFilter: string;
onSelect: (eventId: string) => void;
onApply: (eventId: string) => void;
+ onBrowseArchives: () => void;
}
// 행사 목록 데이터를 받아 그리는 부분. 데이터를 받는 동안은 EventsListScreen의 Suspense가
// EventsListSkeleton을 보여준다. 이동 같은 화면 동작은 Screen이 콜백으로 넘긴다.
-function EventsList({ statusFilter, onSelect, onApply }: EventsListProps) {
+function EventsList({
+ statusFilter,
+ onSelect,
+ onApply,
+ onBrowseArchives,
+}: EventsListProps) {
const events = use(fetchEvents());
// 빌릴게 카테고리 필터와 달리 모집 상태는 목데이터에 이미 들어있어서 실제로 걸러낼 수 있다.
@@ -50,14 +56,21 @@ function EventsList({ statusFilter, onSelect, onApply }: EventsListProps) {
: events.filter((event) => event.status === statusFilter);
if (visibleEvents.length === 0) {
+ const emptyState =
+ EMPTY_STATE_BY_FILTER[statusFilter] ?? EMPTY_STATE_BY_FILTER.all;
return (
// Figma는 Empty State를 목록 영역(헤더·Bottom Nav 사이) 가운데에 둔다.
// 카드 목록과 같은 gap-6 래퍼 안에 넣으면 flex-1이 높이를 못 받아 위에 붙어버려서,
// 빈 목록일 때는 래퍼를 대체해 h-full로 가운데 정렬한다.
);
diff --git a/src/features/events/components/EventsQuestionField.tsx b/src/features/events/components/EventsQuestionField.tsx
index 917555f..3b1a8f8 100644
--- a/src/features/events/components/EventsQuestionField.tsx
+++ b/src/features/events/components/EventsQuestionField.tsx
@@ -2,29 +2,56 @@
// (nodeId 1658:183407, 1658:183418, 1658:183424), 기타 입력칸은 Other Option (nodeId 1658:183954)
import {
Checkbox,
+ css,
RadioGroup,
RadioGroupItem,
TextArea,
TextAreaContent,
Typography,
+ typographyStyle,
} from "@wanteddev/wds";
import { useId } from "react";
+import type {
+ EventsAnswer,
+ EventsChoiceAnswer,
+ EventsQuestion,
+} from "@/entities/events/types";
import {
EVENTS_OTHER_MAX_LENGTH,
EVENTS_OTHER_OPTION_LABEL,
EVENTS_TEXT_MAX_LENGTH,
- type EventsAnswer,
- type EventsChoiceAnswer,
- type EventsQuestion,
} from "@/features/events/constants/eventsApplication";
-interface EventsQuestionFieldProps {
+interface EventsQuestionFieldBaseProps {
question: EventsQuestion;
answer: EventsAnswer | undefined;
- onAnswerChange: (answer: EventsAnswer) => void;
+ /** 취소된 신청내역처럼 입력들을 흐리게(비활성) 보여 준다 */
+ disabled?: boolean;
}
+// 읽기 전용은 readOnly로 명시한다. 입력 폼은 onAnswerChange가 필수라, 콜백을 빠뜨리면
+// 조용히 읽기 전용 폼이 되지 않고 타입 검사에서 걸린다.
+type EventsQuestionFieldProps = EventsQuestionFieldBaseProps &
+ (
+ | {
+ /** 신청내역 상세처럼 제출한 답변을 보여 주기만 한다 */
+ readOnly: true;
+ onAnswerChange?: never;
+ }
+ | {
+ readOnly?: false;
+ onAnswerChange: (answer: EventsAnswer) => void;
+ }
+ );
+
+const TEXT_AREA_TYPOGRAPHY = css`
+ & textarea,
+ & textarea::placeholder {
+ ${typographyStyle("label1-reading", "regular")}
+ }
+`;
+
const EMPTY_CHOICE_ANSWER: EventsChoiceAnswer = { otherText: "", selected: [] };
function toChoiceAnswer(answer: EventsAnswer | undefined): EventsChoiceAnswer {
@@ -34,11 +61,20 @@ function toChoiceAnswer(answer: EventsAnswer | undefined): EventsChoiceAnswer {
// 문항 유형(question.type)별로 컴포넌트를 나누지 않고 이 컴포넌트 하나가 유형에 맞는 입력을 그린다.
// 제목 옆 "*"는 WDS `Label required`를 쓰지 않았다 — Figma의 Field Label은 WDS 인스턴스가 아닌
// 로컬 텍스트이고, `*`도 Body 1/Bold(16px)라 Label이 그리는 label1/medium(14px)과 크기가 다르다.
+//
+// 신청내역 상세(1133:46181)는 같은 카드를 readOnly로 쓴다 — 선택 표시는 그대로 두고 바꿀 수만 없다.
+// 신청취소 상세(1133:46216)는 입력들이 흐린 비활성 모양이라 disabled를 넘긴다.
function EventsQuestionField({
question,
answer,
onAnswerChange,
+ disabled = false,
+ readOnly: isReadOnly = false,
}: EventsQuestionFieldProps) {
+ // 비활성(취소된 신청)이면 선택지 글자도 Label/Disable로 흐리게 한다(Figma 1133:46240)
+ const optionColor = disabled
+ ? "semantic.label.disable"
+ : "semantic.label.normal";
const id = useId();
const titleId = `${id}-title`;
const otherId = `${id}-other`;
@@ -56,7 +92,7 @@ function EventsQuestionField({
option === EVENTS_OTHER_OPTION_LABEL && !checked
? ""
: choiceAnswer.otherText;
- return onAnswerChange({ otherText, selected });
+ return onAnswerChange?.({ otherText, selected });
};
return (
@@ -98,15 +134,22 @@ function EventsQuestionField({
return (
toggleOption(option, checked)}
+ onCheckedChange={
+ isReadOnly
+ ? undefined
+ : (checked) => toggleOption(option, checked)
+ }
size="small"
/>
@@ -122,17 +165,22 @@ function EventsQuestionField({
- toggleOption(EVENTS_OTHER_OPTION_LABEL, checked)
+ onCheckedChange={
+ isReadOnly
+ ? undefined
+ : (checked) =>
+ toggleOption(EVENTS_OTHER_OPTION_LABEL, checked)
}
size="small"
/>
@@ -148,14 +196,16 @@ function EventsQuestionField({
as="input"
className="w-full border-primary border-b-[0.7px] pb-0.5 outline-none placeholder:text-label-assistive"
color="semantic.label.normal"
+ disabled={disabled}
maxLength={EVENTS_OTHER_MAX_LENGTH}
onChange={(event) =>
- onAnswerChange({
+ onAnswerChange?.({
otherText: event.target.value,
selected: choiceAnswer.selected,
})
}
placeholder="기타 내용을 입력해 주세요."
+ readOnly={isReadOnly}
value={choiceAnswer.otherText}
variant="label1-reading"
weight="regular"
@@ -169,6 +219,8 @@ function EventsQuestionField({
{question.type === "singleChoice" && (
@@ -203,10 +255,17 @@ function EventsQuestionField({