Skip to content

Commit d427378

Browse files
RookieANDclaude
andauthored
refactor: 타입 시스템 리팩토링 - enum을 as const 패턴으로 전환 (#110)
* feat: add Category constant with as const pattern - Create new category.ts with CATEGORY, CATEGORY_LABEL constants - Use as const for type inference instead of enum - Add CATEGORY_LIST and CATEGORY_VALUES for iteration Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: standardize rank constants naming - RANKS → RANK_LIST - RANK_LABELS → RANK_LABEL - Add RANK constant object for consistency Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: update constants exports and deprecate food.ts - Export new Category and standardized Rank constants - Add deprecation notice to food.ts (to be removed in next PR) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: restructure type files to use constants - Remove foodCategory.ts (type now in constants) - Update PreferredMenu → PreferredCategory - Update OpinionForm field names (dislikedFoods → dislikedCategories) - Import types from constants instead of separate files BREAKING CHANGE: OpinionForm field names changed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: update schema field names and exports - foodCategorySchema → categorySchema - dislikedFoodSchema → dislikedCategoriesSchema - preferredMenusSchema → preferredCategoriesSchema - Update opinionFormSchema with new field names - Replace hardcoded "ANY" with CATEGORY.ANY BREAKING CHANGE: Schema field names changed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: rename component files to use Category naming - FoodCard → CategoryCard - FoodCategoryCarousel → CategoryCarousel - DislikedFoodButton → DislikedCategoryButton Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: update component export names - FoodCard → CategoryCard - FoodCategoryCarousel → CategoryCarousel - DislikedFoodButton → DislikedCategoryButton - Update component function names to match new file names - Internal logic unchanged (will be updated in next PR) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: update import references to use new constant names - Update all components to use new constant names: * RANKS → RANK_LIST * RANK_LABELS → RANK_LABEL * FOOD_CATEGORIES → CATEGORY_LIST * FOOD_CATEGORY_LABEL → CATEGORY_LABEL * FOOD_CATEGORY_VALUES → CATEGORY_VALUES * dislikedFoodSchema → dislikedCategoriesSchema * preferredMenusSchema → preferredCategoriesSchema - Update type imports: * FoodCategory → Category - Update form field references: * dislikedFoods → dislikedCategories * preferredMenus → preferredCategories - Fix component imports after file renames: * FoodCategoryCarousel → CategoryCarousel * DislikedFoodButton → DislikedCategoryButton This ensures the build succeeds after type system changes. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * style: run prettier format - Format code according to project prettier rules - No logic changes, only formatting Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: 상수 관련 하드코딩 제거 및 deprecated 파일 삭제 (#111) * refactor: remove hardcoded strings and rename variables in DislikedCategoryButton - Import CATEGORY constant - Replace hardcoded "ANY" with CATEGORY.ANY - Rename variables: * food → category * dislikeFoodList → dislikedCategoryList * dislikeFood → dislikedCategory * filteredFoods → filteredCategories * dislikedFoodButtonVariants → dislikedCategoryButtonVariants - Update DislikeStep to use category prop Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: remove hardcoded strings in RankChip, RankSection, and PreferenceStep - Import CATEGORY constant in all three files - Replace all hardcoded "ANY" with CATEGORY.ANY - Rename variables: * newMenus → newCategories * cleanedMenus → cleanedCategories Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: remove hardcoded strings in result components VoteSummarySection.tsx: - Import CATEGORY constant - Replace hardcoded "ANY" with CATEGORY.ANY in category comparisons - Replace hardcoded category strings with CATEGORY constants - Keep distance range strings as-is (DISTANCE_RANGE values are numeric, not strings) OtherCandidateCard.tsx: - Update import: FOOD_CATEGORY_LABEL → CATEGORY_LABEL - Keep "ANY" comparison for distance (not a category constant) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * chore: remove deprecated food.ts - Delete src/constants/gathering/opinion/food.ts - Remove deprecated exports from index.ts - All usages migrated to category.ts Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * style: run prettier format - Format code according to project prettier rules - No logic changes, only formatting Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 61775b6 commit d427378

File tree

23 files changed

+279
-247
lines changed

23 files changed

+279
-247
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export const CATEGORY = {
2+
KOREAN: "KOREAN",
3+
JAPANESE: "JAPANESE",
4+
CHINESE: "CHINESE",
5+
WESTERN: "WESTERN",
6+
ASIAN: "ASIAN",
7+
ANY: "ANY",
8+
} as const;
9+
10+
export type Category = (typeof CATEGORY)[keyof typeof CATEGORY];
11+
12+
export const CATEGORY_LABEL = {
13+
KOREAN: "한식",
14+
JAPANESE: "일식",
15+
CHINESE: "중식",
16+
WESTERN: "양식",
17+
ASIAN: "아시안",
18+
ANY: "상관없음",
19+
} as const;
20+
21+
export const CATEGORY_LIST = [
22+
{ value: CATEGORY.KOREAN, label: CATEGORY_LABEL.KOREAN },
23+
{ value: CATEGORY.JAPANESE, label: CATEGORY_LABEL.JAPANESE },
24+
{ value: CATEGORY.CHINESE, label: CATEGORY_LABEL.CHINESE },
25+
{ value: CATEGORY.WESTERN, label: CATEGORY_LABEL.WESTERN },
26+
{ value: CATEGORY.ASIAN, label: CATEGORY_LABEL.ASIAN },
27+
{ value: CATEGORY.ANY, label: CATEGORY_LABEL.ANY },
28+
] as const;
29+
30+
export const CATEGORY_VALUES = [
31+
CATEGORY.KOREAN,
32+
CATEGORY.JAPANESE,
33+
CATEGORY.CHINESE,
34+
CATEGORY.WESTERN,
35+
CATEGORY.ASIAN,
36+
CATEGORY.ANY,
37+
] as const;

src/constants/gathering/opinion/food.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/constants/gathering/opinion/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ export {
77
type DistanceRange,
88
} from "./distance";
99
export {
10-
FOOD_CATEGORY_LABEL,
11-
FOOD_CATEGORIES,
12-
FOOD_CATEGORY_VALUES,
13-
type FoodCategory,
14-
} from "./food";
15-
export { RANKS, RANK_LABELS } from "./rank";
10+
CATEGORY,
11+
CATEGORY_LABEL,
12+
CATEGORY_LIST,
13+
CATEGORY_VALUES,
14+
type Category,
15+
} from "./category";
16+
export { RANK, RANK_LIST, RANK_LABEL, type RankKey } from "./rank";
1617
export { REGION, REGION_LABEL, REGION_OPTIONS, type Region } from "./region";
1718
export { UI_TEXT } from "./ui-text";
1819
export { MOCK_MEETING_DATA } from "./meeting";

src/constants/gathering/opinion/rank.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import type { RankKey } from "#/types/gathering";
1+
export const RANK = {
2+
FIRST: "first",
3+
SECOND: "second",
4+
THIRD: "third",
5+
} as const;
26

3-
export const RANKS: RankKey[] = ["first", "second", "third"];
7+
export type RankKey = (typeof RANK)[keyof typeof RANK];
48

5-
export const RANK_LABELS: Record<RankKey, string> = {
9+
export const RANK_LIST: ReadonlyArray<RankKey> = [
10+
RANK.FIRST,
11+
RANK.SECOND,
12+
RANK.THIRD,
13+
] as const;
14+
15+
export const RANK_LABEL: Record<RankKey, string> = {
616
first: "1순위",
717
second: "2순위",
818
third: "3순위",

src/hooks/gathering/useOpinionForm.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export function useOpinionForm() {
2626
defaultValues: {
2727
nickname: "",
2828
distanceRange: undefined,
29-
dislikedFoods: [],
30-
preferredMenus: {
29+
dislikedCategories: [],
30+
preferredCategories: {
3131
first: undefined,
3232
second: undefined,
3333
third: undefined,
@@ -38,16 +38,16 @@ export function useOpinionForm() {
3838
const handleSubmit = methods.handleSubmit(async (data) => {
3939
try {
4040
const preferences = compact([
41-
data.preferredMenus.first,
42-
data.preferredMenus.second,
43-
data.preferredMenus.third,
41+
data.preferredCategories.first,
42+
data.preferredCategories.second,
43+
data.preferredCategories.third,
4444
]);
4545

4646
await createParticipant({
4747
accessKey,
4848
preferences,
4949
nickname: data.nickname,
50-
dislikes: data.dislikedFoods,
50+
dislikes: data.dislikedCategories,
5151
distance: distanceRangeToKm(data.distanceRange),
5252
});
5353
router.replace(`/gathering/${accessKey}/opinion/pending`);

src/pageComponents/gathering/opinion/complete/FoodCard.tsx renamed to src/pageComponents/gathering/opinion/complete/CategoryCard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"use client";
22

33
import Image from "next/image";
4-
import type { FoodCategory } from "#/types/gathering";
4+
import type { Category } from "#/types/gathering";
55

6-
interface FoodCardProps {
7-
category: FoodCategory;
6+
interface CategoryCardProps {
7+
category: Category;
88
}
99

10-
export const FoodCard = ({ category }: FoodCardProps) => {
10+
export const CategoryCard = ({ category }: CategoryCardProps) => {
1111
const imageSrc = `/images/foodCategory/${category.toLowerCase()}.svg`;
1212

1313
return (

src/pageComponents/gathering/opinion/complete/FoodCategoryCarousel.tsx renamed to src/pageComponents/gathering/opinion/complete/CategoryCarousel.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,18 @@
33
import { useLayoutEffect, useRef } from "react";
44
import Image from "next/image";
55
import { motion, useMotionValue, animate } from "motion/react";
6-
import { FoodCard } from "./FoodCard";
7-
import { FOOD_CATEGORY_VALUES } from "#/constants/gathering/opinion";
6+
import { CategoryCard } from "./CategoryCard";
7+
import { CATEGORY_VALUES } from "#/constants/gathering/opinion";
88

99
const CARD_WIDTH = 200;
1010
const GAP = 16;
1111

1212
const ANIMATION_DURATION = 0.4;
1313
const PAUSE_DURATION = 600;
1414

15-
export const FoodCategoryCarousel = () => {
16-
const totalCount = FOOD_CATEGORY_VALUES.length;
17-
const items = [
18-
...FOOD_CATEGORY_VALUES,
19-
...FOOD_CATEGORY_VALUES,
20-
...FOOD_CATEGORY_VALUES,
21-
];
15+
export const CategoryCarousel = () => {
16+
const totalCount = CATEGORY_VALUES.length;
17+
const items = [...CATEGORY_VALUES, ...CATEGORY_VALUES, ...CATEGORY_VALUES];
2218

2319
const x = useMotionValue(0);
2420
const indexRef = useRef(totalCount);
@@ -85,7 +81,7 @@ export const FoodCategoryCarousel = () => {
8581
<div className="ygi:w-full ygi:overflow-hidden">
8682
<motion.div className="ygi:flex ygi:gap-4" style={{ x }}>
8783
{items.map((category, index) => (
88-
<FoodCard
84+
<CategoryCard
8985
key={`${category}-${index}`}
9086
category={category}
9187
/>

src/pageComponents/gathering/opinion/complete/CompletePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Toaster } from "#/components/toast";
1010
import { useProceedRecommendResult } from "#/hooks/gathering";
1111

1212
import { SubmissionBottomSheet } from "../SubmissionBottomSheet";
13-
import { FoodCategoryCarousel } from "./FoodCategoryCarousel";
13+
import { CategoryCarousel } from "./CategoryCarousel";
1414
import { ShowResultButton } from "./ShowResultButton";
1515

1616
const PAGE_ID = "의견수합_완료";
@@ -45,7 +45,7 @@ export function CompletePage() {
4545
</StepHeader.Root>
4646
</div>
4747
<div className="ygi:mb-43 ygi:flex ygi:w-full ygi:flex-1 ygi:flex-col ygi:justify-center">
48-
<FoodCategoryCarousel />
48+
<CategoryCarousel />
4949
</div>
5050
</div>
5151
</Layout.Content>

src/pageComponents/gathering/opinion/form/DislikeStep.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { Layout } from "#/components/layout";
77
import { StepIndicator } from "#/components/stepIndicator";
88
import { StepHeader } from "#/components/stepHeader";
99
import { Button } from "#/components/button/Button";
10-
import { DislikedFoodButton } from "./DislikedFoodButton";
10+
import { DislikedCategoryButton } from "./DislikedCategoryButton";
1111
import {
12-
FOOD_CATEGORIES,
12+
CATEGORY_LIST,
1313
OPINION_TOTAL_STEPS,
1414
} from "#/constants/gathering/opinion";
1515
import {
16-
dislikedFoodSchema,
16+
dislikedCategoriesSchema,
1717
type OpinionFormSchema,
1818
} from "#/schemas/gathering";
1919

@@ -36,10 +36,10 @@ const Header = () => {
3636
const Content = () => {
3737
return (
3838
<div className="ygi:flex ygi:flex-wrap ygi:justify-center ygi:gap-3 ygi:pt-6 ygi:pb-9">
39-
{FOOD_CATEGORIES.map((category) => (
40-
<DislikedFoodButton
39+
{CATEGORY_LIST.map((category) => (
40+
<DislikedCategoryButton
4141
key={category.value}
42-
food={category.value}
42+
category={category.value}
4343
/>
4444
))}
4545
</div>
@@ -53,18 +53,19 @@ interface FooterProps {
5353
const Footer = ({ onNext }: FooterProps) => {
5454
const { control } = useFormContext<OpinionFormSchema>();
5555

56-
const { dislikedFoods = [], disabled } = useWatch({
56+
const { dislikedCategories = [], disabled } = useWatch({
5757
control,
58-
name: "dislikedFoods",
59-
compute: (dislikedFoods) => ({
60-
dislikedFoods,
61-
disabled: !dislikedFoodSchema.safeParse(dislikedFoods).success,
58+
name: "dislikedCategories",
59+
compute: (dislikedCategories) => ({
60+
dislikedCategories,
61+
disabled:
62+
!dislikedCategoriesSchema.safeParse(dislikedCategories).success,
6263
}),
6364
});
6465

6566
const handleNext = () => {
66-
const dislikedLabels = dislikedFoods
67-
.map((food) => FOOD_CATEGORIES.find((c) => c.value === food)?.label)
67+
const dislikedLabels = dislikedCategories
68+
.map((food) => CATEGORY_LIST.find((c) => c.value === food)?.label)
6869
.filter(Boolean)
6970
.join(", ");
7071
trackStepComplete({

src/pageComponents/gathering/opinion/form/DislikedFoodButton.tsx renamed to src/pageComponents/gathering/opinion/form/DislikedCategoryButton.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { AnimatePresence, motion } from "motion/react";
66
import { cva } from "class-variance-authority";
77
import { twJoin } from "tailwind-merge";
88

9-
import { FOOD_CATEGORY_LABEL } from "#/constants/gathering/opinion";
9+
import { CATEGORY, CATEGORY_LABEL } from "#/constants/gathering/opinion";
1010
import { XIcon } from "#/icons/xIcon";
1111
import type { OpinionFormSchema } from "#/schemas/gathering";
12-
import type { FoodCategory } from "#/types/gathering";
12+
import type { Category } from "#/types/gathering";
1313

14-
const dislikedFoodButtonVariants = cva(
14+
const dislikedCategoryButtonVariants = cva(
1515
[
1616
"ygi:flex ygi:flex-col ygi:items-center ygi:justify-center",
1717
"ygi:size-[156px] ygi:gap-1 ygi:rounded-full ygi:p-6",
@@ -49,39 +49,43 @@ const dislikedFoodButtonVariants = cva(
4949
},
5050
);
5151

52-
interface DislikedFoodButtonProps {
53-
food: FoodCategory;
52+
interface DislikedCategoryButtonProps {
53+
category: Category;
5454
}
5555

56-
export const DislikedFoodButton = ({ food }: DislikedFoodButtonProps) => {
56+
export const DislikedCategoryButton = ({
57+
category,
58+
}: DislikedCategoryButtonProps) => {
5759
const { control } = useFormContext<OpinionFormSchema>();
58-
const { field } = useController({ name: "dislikedFoods", control });
60+
const { field } = useController({ name: "dislikedCategories", control });
5961

60-
const dislikeFoodList = field.value || [];
62+
const dislikedCategoryList = field.value || [];
6163

62-
const isSelected = dislikeFoodList.includes(food);
63-
const isAny = food === "ANY";
64+
const isSelected = dislikedCategoryList.includes(category);
65+
const isAny = category === CATEGORY.ANY;
6466
const shouldShowXIcon = isSelected && !isAny;
6567

6668
const handleClickDislikeButton = () => {
6769
if (isSelected) {
6870
field.onChange(
69-
dislikeFoodList.filter((dislikeFood) => dislikeFood !== food),
71+
dislikedCategoryList.filter(
72+
(dislikedCategory) => dislikedCategory !== category,
73+
),
7074
);
7175
return;
7276
}
7377

7478
if (isAny) {
75-
field.onChange(["ANY"]);
79+
field.onChange([CATEGORY.ANY]);
7680
return;
7781
}
7882

79-
const filteredFoods = dislikeFoodList.filter(
80-
(dislikeFood) => dislikeFood !== "ANY",
83+
const filteredCategories = dislikedCategoryList.filter(
84+
(dislikedCategory) => dislikedCategory !== CATEGORY.ANY,
8185
);
8286

83-
if (filteredFoods.length < 2) {
84-
field.onChange([...filteredFoods, food]);
87+
if (filteredCategories.length < 2) {
88+
field.onChange([...filteredCategories, category]);
8589
return;
8690
}
8791
};
@@ -90,16 +94,16 @@ export const DislikedFoodButton = ({ food }: DislikedFoodButtonProps) => {
9094
<button
9195
type="button"
9296
aria-pressed={isSelected}
93-
className={dislikedFoodButtonVariants({
97+
className={dislikedCategoryButtonVariants({
9498
isAny,
9599
selected: isSelected,
96100
})}
97101
onClick={handleClickDislikeButton}
98102
>
99103
<div className="ygi:relative ygi:size-20">
100104
<Image
101-
src={`/images/foodCategory/${food.toLowerCase()}.svg`}
102-
alt={FOOD_CATEGORY_LABEL[food]}
105+
src={`/images/foodCategory/${category.toLowerCase()}.svg`}
106+
alt={CATEGORY_LABEL[category]}
103107
fill
104108
className="ygi:object-contain"
105109
priority
@@ -130,7 +134,7 @@ export const DislikedFoodButton = ({ food }: DislikedFoodButtonProps) => {
130134
: "ygi:text-text-secondary",
131135
)}
132136
>
133-
{FOOD_CATEGORY_LABEL[food]}
137+
{CATEGORY_LABEL[category]}
134138
</span>
135139
</button>
136140
);

0 commit comments

Comments
 (0)