Skip to content
137 changes: 80 additions & 57 deletions src/pageComponents/gathering/opinion/result/TasteSummaryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

import { CircleIcon } from "#/icons/circleIcon";
import { XIcon } from "#/icons/xIcon";
import { CATEGORY_LABEL } from "#/constants/gathering/opinion";
import { CATEGORY, CATEGORY_LABEL } from "#/constants/gathering/opinion";
import type { Category } from "#/types/gathering";
import Image from "next/image";

// "ANY"를 제외한 구체적 카테고리 목록
const ALL_FOOD_CATEGORIES: Category[] = [
"KOREAN",
"JAPANESE",
"CHINESE",
"WESTERN",
"ASIAN",
const foodCategories: Category[] = [
CATEGORY.KOREAN,
CATEGORY.JAPANESE,
CATEGORY.CHINESE,
CATEGORY.WESTERN,
CATEGORY.ASIAN,
CATEGORY.ANY,
];

const nonAnyFoodCategories: Category[] = [
CATEGORY.KOREAN,
CATEGORY.JAPANESE,
CATEGORY.CHINESE,
CATEGORY.WESTERN,
CATEGORY.ASIAN,
];

/** 앞 단어 끝 음절의 받침 여부로 을/를 반환 */
Expand Down Expand Up @@ -40,61 +49,75 @@ function computeTasteSummary(
preferences: Record<string, number>,
dislikes: Record<string, number>,
): TasteSummary {
const nonAnyDislikes = ALL_FOOD_CATEGORIES.filter(
(c) => (dislikes[c] ?? 0) >= 1,
);
const nonAnyPrefs = ALL_FOOD_CATEGORIES.filter(
const votedPreferences = foodCategories.filter(
(c) => (preferences[c] ?? 0) >= 1,
);
const allCategoriesDisliked =
nonAnyDislikes.length === ALL_FOOD_CATEGORIES.length;
const votedPreferencesWithoutAny = votedPreferences.filter(
(c) => c !== CATEGORY.ANY,
);
const votedDislikes = foodCategories.filter((c) => (dislikes[c] ?? 0) >= 1);
const votedDislikesWithoutAny = votedDislikes.filter(
(c) => c !== CATEGORY.ANY,
);

const isAllAnyPreferences = votedPreferencesWithoutAny.length === 0;
const isAllAnyDislikes = votedDislikesWithoutAny.length === 0;

// 라인 1: 선호 행
let preferenceHighlight: string;
let preferenceSuffix: string;

if (isAllAnyPreferences) {
preferenceHighlight = "아무 음식";
preferenceSuffix = "이나 상관 없고";
} else if (
!isAllAnyPreferences &&
votedPreferencesWithoutAny.length === 1
) {
Comment on lines +73 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

조건문 !isAllAnyPreferences는 중복 확인으로 보입니다. isAllAnyPreferencesvotedPreferencesWithoutAny.length === 0으로 정의되어 있으므로, else if 블록에 도달했다는 것은 이미 votedPreferencesWithoutAny.length가 0이 아니라는 의미를 내포합니다. 따라서 해당 조건을 제거하여 코드를 더 간결하게 만들 수 있습니다.

	} else if (votedPreferencesWithoutAny.length === 1) {

const foodLabel = CATEGORY_LABEL[votedPreferencesWithoutAny[0]];
preferenceHighlight = foodLabel;
preferenceSuffix = `${getEulReul(foodLabel)} 먹고 싶어하고`;
} else {
preferenceHighlight = formatFoodList(votedPreferencesWithoutAny);
preferenceSuffix = "중";
}

// 라인 1: 불호 행
// 라인 2: 불호 행
let dislikeHighlight: string;
let dislikeSuffix: string;

if (allCategoriesDisliked && nonAnyPrefs.length === 0) {
if (isAllAnyDislikes) {
dislikeHighlight = "피하고 싶은 음식";
dislikeSuffix = "이 모두 다르고";
} else if (allCategoriesDisliked) {
dislikeSuffix = "없어서";
} else if (votedDislikesWithoutAny.length === nonAnyFoodCategories.length) {
dislikeHighlight = "피하고 싶은 음식";
dislikeSuffix = "이 모두 달라서";
} else if (nonAnyDislikes.length === 0 && nonAnyPrefs.length === 1) {
dislikeHighlight = "피하고 싶은 음식";
dislikeSuffix = "없고";
} else {
const lastLabel =
CATEGORY_LABEL[nonAnyDislikes[nonAnyDislikes.length - 1]];
dislikeHighlight = formatFoodList(nonAnyDislikes);
CATEGORY_LABEL[
votedDislikesWithoutAny[votedDislikesWithoutAny.length - 1]
];
dislikeHighlight = formatFoodList(votedDislikesWithoutAny);
dislikeSuffix = `${getEulReul(lastLabel)} 빼고`;
}

// 라인 2: 선호 행
let preferenceHighlight: string;
let preferenceSuffix: string;

if (nonAnyPrefs.length === 0) {
preferenceHighlight = "아무 음식";
preferenceSuffix = "이나 상관 없어서";
} else if (nonAnyPrefs.length === 1) {
const foodLabel = CATEGORY_LABEL[nonAnyPrefs[0]];
preferenceHighlight = foodLabel;
preferenceSuffix = `${getEulReul(foodLabel)} 먹고 싶어해서`;
} else {
preferenceHighlight = formatFoodList(nonAnyPrefs);
preferenceSuffix = "중";
}

// 라인 3: 결론
const safePrefs = nonAnyPrefs.filter((k) => !nonAnyDislikes.includes(k));
const hasConflict = nonAnyPrefs.some((k) => nonAnyDislikes.includes(k));
const safePreferences = votedPreferencesWithoutAny.filter(
(preference) =>
!votedDislikesWithoutAny.some((dislike) => dislike === preference),
);
const hasIntersection = votedPreferencesWithoutAny.some((preference) =>
votedDislikesWithoutAny.some((dislike) => dislike === preference),
);

let conclusion: string;

if (nonAnyDislikes.length === 0 && nonAnyPrefs.length === 1) {
conclusion = `평점 3.5 이상의 ${CATEGORY_LABEL[nonAnyPrefs[0]]} 맛집 추천`;
} else if (hasConflict && safePrefs.length > 0) {
conclusion = `불호가 없는 ${formatFoodList(safePrefs)} 추천`;
if (
votedDislikesWithoutAny.length === 0 &&
votedPreferencesWithoutAny.length === 1
) {
conclusion = `평점 3.5 이상의 ${CATEGORY_LABEL[votedPreferencesWithoutAny[0]]} 맛집 추천`;
} else if (hasIntersection && safePreferences.length > 0) {
conclusion = `불호가 없는 ${formatFoodList(safePreferences)} 추천`;
} else {
conclusion = "평점 3.5 이상의 맛집 추천";
}
Expand Down Expand Up @@ -141,29 +164,29 @@ export const TasteSummaryCard = ({

{/* 텍스트 요약 */}
<div className="ygi:flex ygi:w-full ygi:flex-col ygi:gap-2">
{/* 불호 행 */}
{/* 선호 행 */}
<div className="ygi:flex ygi:w-full ygi:items-center">
<div className="ygi:flex ygi:h-5 ygi:w-5 ygi:shrink-0 ygi:items-center ygi:justify-center ygi:rounded ygi:bg-palette-primary-500">
<XIcon size={11} className="ygi:text-white" />
<div className="ygi:flex ygi:h-5 ygi:w-5 ygi:shrink-0 ygi:items-center ygi:justify-center ygi:rounded ygi:bg-palette-secondary-500">
<CircleIcon size={11} className="ygi:text-white" />
</div>
<span className="ygi:ml-1.5 ygi:shrink-0 ygi:body-16-sb ygi:text-text-interactive">
{dislikeHighlight}
<span className="ygi:ml-1.5 ygi:shrink-0 ygi:body-16-sb ygi:text-palette-secondary-700">
{preferenceHighlight}
</span>
<span className="ygi:ml-1 ygi:body-16-sb ygi:text-text-primary">
{dislikeSuffix}
{preferenceSuffix}
</span>
</div>

{/* 선호 행 */}
{/* 불호 행 */}
<div className="ygi:flex ygi:w-full ygi:items-center">
<div className="ygi:flex ygi:h-5 ygi:w-5 ygi:shrink-0 ygi:items-center ygi:justify-center ygi:rounded ygi:bg-palette-secondary-500">
<CircleIcon size={11} className="ygi:text-white" />
<div className="ygi:flex ygi:h-5 ygi:w-5 ygi:shrink-0 ygi:items-center ygi:justify-center ygi:rounded ygi:bg-palette-primary-500">
<XIcon size={11} className="ygi:text-white" />
</div>
<span className="ygi:ml-1.5 ygi:shrink-0 ygi:body-16-sb ygi:text-palette-secondary-700">
{preferenceHighlight}
<span className="ygi:ml-1.5 ygi:shrink-0 ygi:body-16-sb ygi:text-text-interactive">
{dislikeHighlight}
</span>
<span className="ygi:ml-1 ygi:body-16-sb ygi:text-text-primary">
{preferenceSuffix}
{dislikeSuffix}
</span>
</div>

Expand Down
24 changes: 16 additions & 8 deletions src/pageComponents/gathering/opinion/result/VoteSummarySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,19 +216,27 @@ const PreferenceVoteBlock = ({
? [unanimousCategory]
: voted.filter((c) => c !== CATEGORY.ANY);

// progress bar 세그먼트 (노출 순서 기준, votes 있는 것만)
const barCategories = foodCategoryOrder.filter(
(c) => (preferences[c] ?? 0) >= 1,
);
// progress bar 세그먼트 (투표 수 내림차순, ANY 마지막, 동률은 고정 순서 유지)
const barCategories = foodCategoryOrder
.filter((c) => (preferences[c] ?? 0) >= 1)
.sort((a, b) => {
if (a === CATEGORY.ANY) return 1;
if (b === CATEGORY.ANY) return -1;
return (preferences[b] ?? 0) - (preferences[a] ?? 0);
});
const totalVotes = barCategories.reduce(
(sum, c) => sum + (preferences[c] ?? 0),
0,
);

// 카테고리 dot 목록 (ANY 포함, 순서 유지, count > 0만)
const listCategories = foodCategoryOrder.filter(
(c) => (preferences[c] ?? 0) >= 1,
);
// 카테고리 dot 목록 (투표 수 내림차순, ANY 마지막, 동률은 고정 순서 유지)
const listCategories = foodCategoryOrder
.filter((c) => (preferences[c] ?? 0) >= 1)
.sort((a, b) => {
if (a === CATEGORY.ANY) return 1;
if (b === CATEGORY.ANY) return -1;
return (preferences[b] ?? 0) - (preferences[a] ?? 0);
});
Comment on lines +219 to +239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

barCategorieslistCategories를 생성하는 로직이 동일하게 중복되고 있습니다. 공통 로직을 sortedVotedCategories 변수로 추출하여 중복을 제거하고, 주석을 정리하여 코드 가독성과 유지보수성을 높이는 것이 좋습니다. 이는 스타일 가이드의 DRY(Don't Repeat Yourself) 원칙(493번 라인)에도 부합합니다.

	// progress bar 및 dot 목록에 사용할 카테고리 (투표 수 내림차순, ANY 마지막, 동률은 고정 순서 유지)
	const sortedVotedCategories = foodCategoryOrder
		.filter((c) => (preferences[c] ?? 0) >= 1)
		.sort((a, b) => {
			if (a === CATEGORY.ANY) return 1;
			if (b === CATEGORY.ANY) return -1;
			return (preferences[b] ?? 0) - (preferences[a] ?? 0);
		});
	const barCategories = sortedVotedCategories;
	const totalVotes = barCategories.reduce(
		(sum, c) => sum + (preferences[c] ?? 0),
		0,
	);

	const listCategories = sortedVotedCategories;
References
  1. 스타일 가이드 493번 라인에서는 코드 중복을 피하도록 권장하고 있습니다 (DRY 원칙). (link)


return (
<div className="ygi:flex ygi:flex-col ygi:gap-4 ygi:rounded-md ygi:bg-surface-white ygi:p-5">
Expand Down
Loading