Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1ec2b2f
feat: 마인드맵 생성 페이지 추가
pakxe Feb 15, 2026
59ce201
feat: 마인드맵 생성 훅 추가
pakxe Feb 15, 2026
c87c974
feat: 이모지 카드 컴포넌트 추가
pakxe Feb 15, 2026
3218fe5
feat: selectFrom 타입 추가
pakxe Feb 15, 2026
b830bf2
feat: 마인드맵 타입 카드 컴포넌트 구현
pakxe Feb 15, 2026
b528c08
feat: CreateMindmapPage 연결
pakxe Feb 15, 2026
beffaa5
feat: 새로운 마인드맵의 타입을 선택하는 step page 구현
pakxe Feb 15, 2026
43e10bc
feat: 새로운 마인드맵의 카테고리를 선택하는 페이지 구현
pakxe Feb 15, 2026
0500901
feat: 퍼널 타입
pakxe Feb 15, 2026
a19b385
feat: 팀 마인드맵 디테일 작성을 위한 step page 구현
pakxe Feb 15, 2026
bbfe7f8
design: mb 추가
pakxe Feb 15, 2026
10ae65f
fix: className을 제일 상위에서만 받도록 수정
pakxe Feb 15, 2026
4ebd3eb
feat: utility type 들 추가
pakxe Feb 15, 2026
bf7daf8
feat: bg 이미지 추가
pakxe Feb 15, 2026
8defb03
chore: assets alias 추가
pakxe Feb 15, 2026
4226515
feat: Landing info 추가
pakxe Feb 15, 2026
4c4d43b
feat: Logo 추가
pakxe Feb 15, 2026
23ece4d
chore: 불필요한 파일 삭제
pakxe Feb 15, 2026
1d0f7d9
design: GNB 블러 추가
pakxe Feb 15, 2026
eba3e40
design: hover transition 추가
pakxe Feb 15, 2026
2d9a749
feat: useFunnel 훅 구현
pakxe Feb 15, 2026
ef58de5
design: scroll 되도록 수정
pakxe Feb 15, 2026
1f72200
feat: path 추가
pakxe Feb 15, 2026
9c2bb49
refactor: funnel.exit 쓰도록 수정
pakxe Feb 15, 2026
2d09860
feat: MindmapCard에 interaction 추가
pakxe Feb 15, 2026
7e30d40
chore: MindmapDetailPage로 이름 변경
pakxe Feb 15, 2026
627d654
chore: 사용하지 않는 파일 제거
pakxe Feb 15, 2026
3400c4f
chore: 사용하지 않는 파일 제거
pakxe Feb 15, 2026
e5b495e
fix: console.error로 수정
pakxe Feb 16, 2026
c686139
style: 중괄호 추가
pakxe Feb 16, 2026
068f3bd
refactor: cn사용
pakxe Feb 16, 2026
8c28f15
refactor: cn사용
pakxe Feb 16, 2026
b601d6a
fix: import 수정
pakxe Feb 16, 2026
307d6c0
fix: 프로퍼티명 수정
pakxe Feb 16, 2026
5a0c047
refactor: icon logo로 수정
pakxe Feb 16, 2026
6e617b7
feat: mindmap page 추가
pakxe Feb 16, 2026
5cc66c5
chore: 충돌 병합
pakxe Feb 16, 2026
1f44f29
chore: 병합
pakxe Feb 16, 2026
fc93f21
chore: 임시 주석 처리
pakxe Feb 16, 2026
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
42 changes: 21 additions & 21 deletions frontend/components.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/shared/components",
"utils": "@/shared/lib/utils",
"ui": "@/shared/components/ui",
"lib": "@/shared/lib",
"hooks": "@/shared/hooks"
},
"registries": {}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/shared/components",
"utils": "@/shared/lib/utils",
"ui": "@/shared/components/ui",
"lib": "@/shared/lib",
"hooks": "@/shared/hooks"
},
"registries": {}
}
10 changes: 5 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AuthProvider } from "@/features/auth/providers/AuthProvider";
import EpisodeArchivePage from "@/features/episode_archive/pages/EpisodeArchivePage";
import HomePage from "@/features/home/pages/HomePage";
import LandingPage from "@/features/landing/pages/LandingPage";
// import CreateMindmapPage from "@/features/mindmap/pages/CreateMindmapPage";
import CreateMindmapFunnelPage from "@/features/mindmap/pages/CreateMindmapPage";
import MindmapListPage from "@/features/mindmap/pages/MindmapListPage";
import MindmapPage from "@/features/mindmap/pages/MindmapPage";
import SelfDiagnosisPage from "@/features/self_diagnosis/pages/SelfDiagnosisPage";
Expand Down Expand Up @@ -45,10 +45,10 @@ const router = createBrowserRouter([
path: PATHS.self_diagnosis.list,
element: <SelfDiagnosisPage />,
},
// {
// path: PATHS.mindmap.create,
// element: <CreateMindmapPage />,
// },
{
path: PATHS.mindmap.create,
element: <CreateMindmapFunnelPage />,
},
{
path: PATHS.mindmap.detail,
element: <MindmapPage />,
Expand Down
Binary file added frontend/src/assets/img/img_landing_episode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/img/img_landing_main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/img/img_landing_mindmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/img/img_landing_selftest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 25 additions & 30 deletions frontend/src/features/mindmap/components/MindmapCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef, useState } from "react";
import { useNavigate } from "react-router";

import { useDeleteMindmap } from "@/features/mindmap/hooks/useDeleteMindmap";
import { useUpdateMindmapFavorite } from "@/features/mindmap/hooks/useUpdateMindmapFavorite"; // 훅 import
import { useUpdateMindmapFavorite } from "@/features/mindmap/hooks/useUpdateMindmapFavorite";
import { useUpdateMindmapName } from "@/features/mindmap/hooks/useUpdateMindmapName";
import { MindmapItem, MindmapType } from "@/features/mindmap/types/mindmap";
import Button from "@/shared/components/button/Button";
Expand All @@ -21,15 +21,29 @@ import { getRelativeTime } from "@/utils/get_relative_time";
type Props = {
data: MindmapItem;
type?: MindmapType;

interaction?: "navigate" | "select";
selected?: boolean;
onSelect?: (mindmapId: string) => void;

className?: string;
};

const MindmapCard = ({ data, type = "PUBLIC" }: Props) => {
const MindmapCard = ({ data, type = "PUBLIC", interaction = "navigate", selected, onSelect, className }: Props) => {
const { mutate: deleteMindmap } = useDeleteMindmap();
const { mutate: updateMindmapName } = useUpdateMindmapName();
const { mutate: updateMindmapFavorite } = useUpdateMindmapFavorite();

const navigate = useNavigate();

const handleCardClick = () => {
if (interaction === "select") {
onSelect?.(data.mindmapId);
return;
}
navigate(linkTo.mindmap.detail(data.mindmapId));
};

const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
if (window.confirm("정말 이 마인드맵을 삭제하시겠습니까?")) {
Expand All @@ -54,15 +68,12 @@ const MindmapCard = ({ data, type = "PUBLIC" }: Props) => {
if (value.length <= 20) {
setEditName(value);
} else {
// 20자가 넘어가면 잘라냄 (붙여넣기 대응)
setEditName(value.slice(0, 20));
}
};

useClickOutside(editContainerRef, () => {
if (isEditing) {
handleCancel();
}
if (isEditing) handleCancel();
});

const handleCancel = () => {
Expand Down Expand Up @@ -97,10 +108,12 @@ const MindmapCard = ({ data, type = "PUBLIC" }: Props) => {

return (
<Card
className="min-h-50 overflow-hidden w-full"
onClick={() => {
navigate(linkTo.mindmap.detail(data.mindmapId));
}}
className={cn(
"min-h-50 overflow-hidden w-full",
selected && "outline-primary outline-2 shadow-md",
className,
)}
onClick={handleCardClick}
header={
<div className="flex items-center justify-between relative h-8">
{isEditing ? (
Expand Down Expand Up @@ -163,32 +176,14 @@ const MindmapCard = ({ data, type = "PUBLIC" }: Props) => {
}
>
<button className="p-1.5 hover:bg-gray-100 rounded-full flex items-center justify-center">
<Icon name="ic_ellipsis_vertical" color="var(--color-gray-500)" />
<Icon name="ic_ellipsis_vertical" />
</button>
</Popover>
</div>
)}
</div>
}
bottomContents={
<div className="flex flex-wrap gap-1.5 mt-2">
{/* {displayTags.map((tag, index) => (
<Chip
key={index}
variant="secondary"
size="sm"
className="bg-blue-50 text-blue-600 font-medium border-0"
>
{tag}
</Chip>
))}
{hiddenTagCount > 0 && (
<Chip variant="secondary" size="sm" className="bg-blue-50 text-blue-600 font-medium border-0">
+{hiddenTagCount}
</Chip>
)} */}
</div>
}
bottomContents={<div className="flex flex-wrap gap-1.5 mt-2" />}
footer={
<div className="flex items-center justify-between text-body-14-regular text-gray-500 pt-4 border-t border-transparent">
<span>{getRelativeTime(data.updatedAt)}</span>
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/features/mindmap/components/MindmapCategoryStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useCreateMindmap } from "@/features/mindmap/hooks/useCreateMindmap";
import { ACTIVITY_CATEGORIES, ActivityCategory } from "@/features/mindmap/types/mindmap";
import { CreateMindmapFunnel } from "@/features/mindmap/types/mindmap_funnel";
import BottomSticky from "@/shared/components/bottom_sticky/BottomSticky";
import Button from "@/shared/components/button/Button";
import { EmojiCard } from "@/shared/components/emoji_card/EmojiCard";
import Top from "@/shared/components/top/Top";
import { FunnelInstance } from "@/shared/hooks/useFunnel";
import { linkTo } from "@/shared/utils/route";

type CategoryStepFunnel = Extract<FunnelInstance<CreateMindmapFunnel>, { step: "CATEGORY" }>;

export function MindmapCategoryStep({ funnel }: { funnel: CategoryStepFunnel }) {
const { mutate: createMindmap, isPending } = useCreateMindmap();

const selected = funnel.context.categories ?? [];

const toggle = (id: ActivityCategory) => {
funnel.history.setContext((prev) => {
const prevSelected = prev.categories ?? [];
const next = prevSelected.includes(id) ? prevSelected.filter((x) => x !== id) : [...prevSelected, id];
return { ...prev, categories: next };
});
};

const canSubmit = selected.length > 0;

const handleSubmit = () => {
createMindmap(
{
isShared: false,
title: "새로운 마인드맵",
},
{
onSuccess: (data) => {
funnel.exit(linkTo.mindmap.detail(data.mindmap.mindmapId), { replace: false });
},
},
);
};

return (
<>
<div className="flex flex-col flex-1 w-full">
<Top
lowerGap="lg"
title={
<h1 className="typo-title-28-bold text-text-main1 leading-snug">
마인드맵으로 정리할 <br /> 활동 카테고리를 선택하세요.
</h1>
}
lower={
<p className="text-text-main2 typo-body-16-medium">
사용 목적에 따라 알맞는 마인드맵 유형을 선택해 주세요.
</p>
}
className="mb-12"
/>

<div className="grid grid-cols-2 gap-4 pb-10">
{ACTIVITY_CATEGORIES.map((c) => (
<EmojiCard
key={c.id}
emoji={c.emoji}
label={c.label}
selected={selected.includes(c.id)}
onClick={() => toggle(c.id)}
/>
))}
</div>
</div>

<BottomSticky>
<Button
size="md"
variant="primary"
layout="fullWidth"
disabled={!canSubmit || isPending}
onClick={handleSubmit}
>
{isPending ? "생성 중..." : "완료"}
</Button>
</BottomSticky>
</>
);
}
44 changes: 44 additions & 0 deletions frontend/src/features/mindmap/components/MindmapTypeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Card from "@/shared/components/card/Card";
import Icon, { IconName } from "@/shared/components/icon/Icon";
import { cn } from "@/utils/cn";

type Props = {
icon: IconName;
title: string;
description: string;
isSelected: boolean;
onClick: () => void;
};

const MindmapTypeCard = ({ icon, title, description, isSelected, onClick }: Props) => {
return (
<Card
className={cn(
"relative cursor-pointer flex-1 h-75 transition-all py-12.5 px-10.5",
isSelected
? "outline-primary outline-2 shadow-md bg-cobalt-100/30 text-primary"
: "text-text-sub1 hover:bg-gray-50",
)}
onClick={onClick}
header={
<div className="flex flex-col gap-4 mt-2">
{isSelected && (
<div className="absolute top-6 right-6">
<Icon name="ic_check" color="currentColor" size={24} />
</div>
)}

<Icon name={icon} size={36} color="currentColor" />
<h2 className="typo-title-22-bold">{title}</h2>
</div>
}
contents={
<div className="mt-auto pt-4">
<p className="typo-body-15-medium leading-relaxed break-keep">{description}</p>
</div>
}
/>
);
};

export default MindmapTypeCard;
75 changes: 75 additions & 0 deletions frontend/src/features/mindmap/components/MindmapTypeStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import MindmapTypeCard from "@/features/mindmap/components/MindmapTypeCard";
import { CreateMindmapFunnel } from "@/features/mindmap/types/mindmap_funnel";
import BottomSticky from "@/shared/components/bottom_sticky/BottomSticky";
import Button from "@/shared/components/button/Button";
import Top from "@/shared/components/top/Top";
import { FunnelInstance } from "@/shared/hooks/useFunnel";

type TypeStepFunnel = Extract<FunnelInstance<CreateMindmapFunnel>, { step: "TYPE" }>;

const MINDMAP_TYPES = [
{
id: "PRIVATE" as const,
icon: "ic_user" as const,
title: "개인 마인드맵",
description: "나만 사용할 수 있는 마인드맵이에요.\n개인 경험 정리와 자기소개서 준비에 적합해요.",
},
{
id: "PUBLIC" as const,
icon: "ic_team" as const,
title: "팀 마인드맵",
description: "함께 사용하는 마인드맵이에요.\n프로젝트를 진행한 팀원들과 경험을 정리할 수 있어요.",
},
] as const;

export function MindmapTypeStep({ funnel }: { funnel: TypeStepFunnel }) {
const selectedType = funnel.context.mindmapType;

return (
<>
<div className="flex-1">
<Top
lowerGap="lg"
title={<h1 className="typo-title-28-bold text-text-main1">어떤 마인드맵을 만들까요?</h1>}
lower={
<p className="typo-body-16-medium text-text-main2">
사용 목적에 따라 알맞는 마인드맵 유형을 선택해 주세요.
</p>
}
className="mb-12"
/>

<div className="flex w-full gap-6 mb-12">
{MINDMAP_TYPES.map((type) => (
<MindmapTypeCard
key={type.id}
icon={type.icon}
title={type.title}
description={type.description}
isSelected={selectedType === type.id}
onClick={() => funnel.history.setContext({ mindmapType: type.id })}
/>
))}
</div>
</div>

<BottomSticky>
<Button
size="md"
variant="primary"
layout="fullWidth"
disabled={!selectedType}
onClick={() => {
if (selectedType === "PUBLIC") {
funnel.history.push("TEAM_DETAIL", { mindmapType: "PUBLIC", episodes: ["", "", ""] });
} else {
funnel.history.push("CATEGORY", { mindmapType: "PRIVATE" });
}
}}
>
마인드맵 만들기
</Button>
</BottomSticky>
</>
);
}
Loading