Merge pull request '[fix] 디렉토리 구조 변경 및 파일명칭 수정' (#5) from fix/structure into master

Reviewed-on: #5
master
minheon 2026-04-02 01:56:33 +00:00
commit 66c87eaaf8
95 changed files with 160 additions and 164 deletions

View File

@ -10,25 +10,36 @@
## 디렉토리 구조
디렉토리 구조는 다음을 따를 예정입니다.
```bash
src/
├── app/ # 애플리케이션 진입점 및 전역 설정 (Router, Providers, 글로벌 스타일)
├── assets/ # 정적 파일 (이미지, 폰트, 로티 애니메이션 등)
├── components/ # 도메인에 종속되지 않는 공통 UI 컴포넌트 (버튼, 모달, 디자인 시스템)
│ └── providers/ # React Context Provider 모음 (QueryProvider 등)
├── assets/ # 정적 파일 (이미지, 폰트, SVG 아이콘 등)
├── components/ # 도메인에 종속되지 않는 공통 UI 컴포넌트 (버튼, 카드, 디자인 시스템)
├── features/ # 핵심 비즈니스 로직 및 도메인 영역 (이 구조의 핵심)
│ ├── auth/ # 특정 도메인 (예: 인증)
│ │ ├── api/ # 해당 도메인 전용 API 통신 함수
│ ├── home/ # 홈(랜딩) 도메인
│ │ ├── content/ # 해당 도메인 전용 UI 텍스트·카피 (정적 콘텐츠)
│ │ ├── hooks/ # 해당 도메인 전용 커스텀 훅
│ │ ├── store/ # 해당 도메인 전용 상태 (Zustand 등)
│ │ └── ui/ # 해당 도메인 전용 UI 컴포넌트
│ ├── report/ # 리포트 도메인
│ │ ├── config/ # 섹션 ID·레이블 등 UI 설정값
│ │ ├── hooks/ # 해당 도메인 전용 커스텀 훅
│ │ ├── mocks/ # API 연동 전 임시 목업 데이터
│ │ ├── types/ # 해당 도메인 전용 타입 정의
│ │ └── ui/ # 해당 도메인 전용 UI 컴포넌트
├── hooks/ # 전역에서 사용하는 공통 훅 (useClickOutside 등)
├── layouts/ # 페이지 레이아웃 (GNB, Sidebar, 풋터 등)
├── pages/ # 라우팅과 1:1 매칭되는 페이지 진입점 (여기서는 features의 컴포넌트만 조립)
│ └── plan/ # 마케팅 플랜 도메인 (report와 동일한 구조)
│ ├── config/
│ ├── hooks/
│ ├── mocks/
│ ├── types/
│ └── ui/
├── hooks/ # 전역에서 사용하는 공통 훅 (useInView 등)
├── layouts/ # 페이지 레이아웃 (GNB, SubNav, Footer 등)
├── pages/ # 라우팅과 1:1 매칭되는 페이지 진입점 (features의 컴포넌트만 조립)
├── services/ # 공통 API 클라이언트 설정 (Axios 인스턴스, 인터셉터 등)
├── store/ # 전역 상태 관리 (사용자 세션, 테마 등)
└── utils/ # 공통 유틸리티 함수 (날짜 포맷팅, 정규식 등)
├── types/ # 여러 도메인에서 공유하는 공통 타입 정의
└── utils/ # 공통 유틸리티 함수 (숫자 포맷팅, URL 처리 등)
```
## 시작하기

View File

@ -1,6 +1,6 @@
import type { CSSProperties } from "react";
import EyeIcon from "@/assets/icons/eye.svg?react";
import { formatCompactNumber } from "@/lib/formatNumber";
import { formatCompactNumber } from "@/utils/formatNumber";
export type TopVideoCardProps = {
title: string;

View File

@ -1,6 +1,4 @@
export { ComparisonRow, type ComparisonRowProps } from "@/components/compare/ComparisonRow";
export { BrandConsistencyMap, type BrandConsistencyMapProps } from "@/components/brand/BrandConsistencyMap";
export { OtherChannelRow, type OtherChannelRowProps } from "@/components/channel/OtherChannelRow";
export { SeverityBadge, type SeverityBadgeProps } from "@/components/badge/SeverityBadge";
export { ChannelScoreCard, type ChannelScoreCardProps } from "@/components/card/ChannelScoreCard";
export { InfoStatCard, type InfoStatCardProps } from "@/components/card/InfoStatCard";
@ -8,7 +6,6 @@ export { MetricCard, type MetricCardProps } from "@/components/card/MetricCard";
export { PixelInstallCard, type PixelInstallCardProps } from "@/components/card/PixelInstallCard";
export { TopVideoCard, type TopVideoCardProps } from "@/components/card/TopVideoCard";
export { TagChipList, type TagChipListProps } from "@/components/chip/TagChipList";
export { DiagnosisRow, type DiagnosisRowProps } from "@/components/diagnosis/DiagnosisRow";
export { ConsolidationCallout, type ConsolidationCalloutProps } from "@/components/panel/ConsolidationCallout";
export { HighlightPanel, type HighlightPanelProps } from "@/components/panel/HighlightPanel";
export { ScoreRing, type ScoreRingProps } from "@/components/rating/ScoreRing";

View File

@ -1,5 +1,5 @@
import StarIcon from "@/assets/icons/star.svg?react";
import { formatCompactNumber } from "@/lib/formatNumber";
import { formatCompactNumber } from "@/utils/formatNumber";
export type StarRatingDisplayProps = {
rating: number;

View File

@ -5,7 +5,7 @@ import {
CTA_FOOTNOTE,
CTA_HEADLINE,
CTA_URL_PLACEHOLDER,
} from "@/features/home/constants/cta_contents";
} from "@/features/home/content/cta";
import { useAnalyze } from "@/features/home/hooks/useAnalyze";
import { useInView } from "@/hooks/useInView";

View File

@ -7,7 +7,7 @@ import {
HERO_LEAD_EN,
HERO_LEAD_KO,
HERO_URL_PLACEHOLDER,
} from "@/features/home/constants/hero_contents";
} from "@/features/home/content/hero";
import { useAnalyze } from "@/features/home/hooks/useAnalyze";
export function HeroSection() {

View File

@ -1,4 +1,4 @@
import { PROBLEM_CARDS, PROBLEM_CARD_STAGGER } from "@/features/home/constants/problem_contents";
import { PROBLEM_CARDS, PROBLEM_CARD_STAGGER } from "@/features/home/content/problem";
import { useInView } from "@/hooks/useInView";
export function ProblemSection() {

View File

@ -1,4 +1,4 @@
import { SOLUTION_CARDS } from "@/features/home/constants/solution_contents";
import { SOLUTION_CARDS } from "@/features/home/content/solution";
import { useInView } from "@/hooks/useInView";
export function SolutionSection() {

View File

@ -1,4 +1,4 @@
import { CORE_MODULES, MODULE_CARD_STAGGER } from "@/features/home/constants/modules_contents";
import { CORE_MODULES, MODULE_CARD_STAGGER } from "@/features/home/content/modules";
import { useInView } from "@/hooks/useInView";
import { CoreModuleCard } from "./system/CoreModuleCard";
import { CoreModulesCenterHeading } from "./system/CoreModulesCenterHeading";

View File

@ -1,5 +1,5 @@
import CheckCircleIcon from "@/assets/home/check-circle.svg?react";
import { USE_CASE_CARDS } from "@/features/home/constants/use_cases_contents";
import { USE_CASE_CARDS } from "@/features/home/content/useCases";
import { useInView } from "@/hooks/useInView";
export function UseCaseSection() {

View File

@ -1,4 +1,4 @@
import { AGDP_NODES } from "@/features/home/constants/process_contents";
import { AGDP_NODES } from "@/features/home/content/process";
import { AgdpOrbitNode } from "./AgdpOrbitNode";
import { AgdpRewardPathLabel } from "./AgdpRewardPathLabel";

View File

@ -1,5 +1,5 @@
import type { AgdpNodeDef } from "@/features/home/constants/process_contents";
import { AGDP_SLOT_WRAPPER_CLASS } from "@/features/home/constants/process_contents";
import type { AgdpNodeDef } from "@/features/home/content/process";
import { AGDP_SLOT_WRAPPER_CLASS } from "@/features/home/content/process";
type Props = { node: AgdpNodeDef };

View File

@ -1,4 +1,4 @@
import type { CoreModule } from "@/features/home/constants/modules_contents";
import type { CoreModule } from "@/features/home/content/modules";
type Props = {
mod: CoreModule;

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { MOCK_PLAN } from "@/features/plan/constants/mock_plan";
import { MOCK_PLAN } from "@/features/plan/mocks/plan";
import type { MarketingPlan } from "@/features/plan/types/marketingPlan";
type UseMarketingPlanResult = {

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { useMainSubNav } from "@/layouts/MainSubNavLayout";
import type { SubNavItem } from "@/layouts/SubNav";
import { PLAN_SECTIONS } from "@/features/plan/constants/plan_sections";
import { PLAN_SECTIONS } from "@/features/plan/config/planSections";
export function usePlanSubNav() {
const { setSubNav } = useMainSubNav();

View File

@ -1,44 +0,0 @@
import { useCurrentMarketingPlan } from "@/features/plan/hooks/useCurrentMarketingPlan";
import { usePlanSubNav } from "@/features/plan/hooks/usePlanSubNav";
import { PlanAssetCollectionSection } from "@/features/plan/ui/PlanAssetCollectionSection";
import { PlanBrandingGuideSection } from "@/features/plan/ui/PlanBrandingGuideSection";
import { PlanChannelStrategySection } from "@/features/plan/ui/PlanChannelStrategySection";
import { PlanContentCalendarSection } from "@/features/plan/ui/PlanContentCalendarSection";
import { PlanContentStrategySection } from "@/features/plan/ui/PlanContentStrategySection";
import { PlanCtaSection } from "@/features/plan/ui/PlanCtaSection";
import { PlanHeaderSection } from "@/features/plan/ui/PlanHeaderSection";
import { PlanMyAssetUploadSection } from "@/features/plan/ui/PlanMyAssetUploadSection";
export function MarketingPlanPage() {
const { data, error } = useCurrentMarketingPlan();
usePlanSubNav();
if (error || !data) {
return (
<div className="min-h-[50vh] flex items-center justify-center px-6">
<div className="text-center max-w-md">
<p className="title-16 text-[var(--color-status-critical-text)] mb-2 break-keep">
</p>
<p className="body-14 text-neutral-60 break-keep">
{error ?? "마케팅 플랜을 찾을 수 없습니다."}
</p>
</div>
</div>
);
}
return (
<div data-plan-content data-plan-id={data.id}>
<PlanHeaderSection />
<PlanBrandingGuideSection />
<PlanChannelStrategySection />
<PlanContentStrategySection />
<PlanContentCalendarSection />
<PlanAssetCollectionSection />
<PlanMyAssetUploadSection />
<PlanCtaSection />
</div>
);
}

View File

@ -2,7 +2,7 @@ import { useCurrentMarketingPlan } from "@/features/plan/hooks/useCurrentMarketi
import { PlanHeaderDaysBadge } from "@/features/plan/ui/header/PlanHeaderDaysBadge";
import { PlanHeaderHeroBlobs } from "@/features/plan/ui/header/PlanHeaderHeroBlobs";
import { PlanHeaderHeroColumn } from "@/features/plan/ui/header/PlanHeaderHeroColumn";
import { PLAN_HEADER_BG_CLASS } from "@/features/plan/ui/header/planHeaderSectionStyles";
import { PLAN_HEADER_BG_CLASS } from "@/features/plan/ui/header/planHeaderSectionClasses";
export function PlanHeaderSection() {
const { data, error } = useCurrentMarketingPlan();

View File

@ -14,7 +14,7 @@ import {
assetTypeBadgeClass,
assetTypeDisplayLabel,
formatYoutubeViews,
} from "@/features/plan/ui/assetCollection/assetCollectionBadgeClass";
} from "@/features/plan/ui/assetCollection/assetCollectionBadgeClasses";
type AssetCollectionPanelProps = {
data: AssetCollectionData;

View File

@ -4,7 +4,7 @@ import { BrandingChannelIcon } from "@/features/plan/ui/branding/BrandingChannel
import {
brandingChannelStatusBadgeClass,
brandingChannelStatusLabel,
} from "@/features/plan/ui/branding/brandingChannelStatusClass";
} from "@/features/plan/ui/branding/brandingChannelStatusClasses";
type BrandingChannelRulesTabProps = {
channels: BrandGuide["channelBranding"];

View File

@ -2,7 +2,7 @@ import { useState } from "react";
import { BrandConsistencyMap } from "@/components/brand/BrandConsistencyMap";
import { SegmentTabButton } from "@/features/plan/ui/SegmentTabButton";
import type { BrandGuide } from "@/features/plan/types/marketingPlan";
import { BRANDING_GUIDE_TAB_ITEMS, type BrandingGuideTabKey } from "@/features/plan/ui/branding/brandingTabItems";
import { BRANDING_GUIDE_TAB_ITEMS, type BrandingGuideTabKey } from "@/features/plan/ui/branding/brandingTabs";
import { BrandingChannelRulesTab } from "@/features/plan/ui/branding/BrandingChannelRulesTab";
import { BrandingToneVoiceTab } from "@/features/plan/ui/branding/BrandingToneVoiceTab";
import { BrandingVisualIdentityTab } from "@/features/plan/ui/branding/BrandingVisualIdentityTab";

View File

@ -3,7 +3,7 @@ import { Pill } from "@/components/atoms/Pill";
import { Surface } from "@/components/atoms/Surface";
import type { ChannelStrategyCard } from "@/features/plan/types/marketingPlan";
import { BrandingChannelIcon } from "@/features/plan/ui/branding/BrandingChannelIcon";
import { channelStrategyPriorityPillClass } from "@/features/plan/ui/channelStrategy/channelStrategyPillClass";
import { channelStrategyPriorityPillClass } from "@/features/plan/ui/channelStrategy/channelStrategyPillClasses";
type ChannelStrategyGridProps = {
channels: ChannelStrategyCard[];

View File

@ -5,7 +5,7 @@ import {
CALENDAR_CONTENT_TYPE_LABELS,
CALENDAR_DAY_HEADERS,
calendarContentTypeVisual,
} from "@/features/plan/ui/contentCalendar/calendarContentTypeVisual";
} from "@/features/plan/ui/contentCalendar/calendarContentTypeClasses";
import { ContentCalendarTypeIcon } from "@/features/plan/ui/contentCalendar/ContentCalendarTypeIcon";
type ContentCalendarPanelProps = {

View File

@ -4,7 +4,7 @@ import type { ContentStrategyData } from "@/features/plan/types/marketingPlan";
import {
CONTENT_STRATEGY_TAB_ITEMS,
type ContentStrategyTabKey,
} from "@/features/plan/ui/contentStrategy/contentStrategyTabItems";
} from "@/features/plan/ui/contentStrategy/contentStrategyTabs";
import { ContentStrategyPillarsTab } from "@/features/plan/ui/contentStrategy/ContentStrategyPillarsTab";
import { ContentStrategyRepurposingTab } from "@/features/plan/ui/contentStrategy/ContentStrategyRepurposingTab";
import { ContentStrategyTypesTab } from "@/features/plan/ui/contentStrategy/ContentStrategyTypesTab";

View File

@ -1,6 +1,6 @@
import { Pill } from "@/components/atoms/Pill";
import type { ContentTypeRow } from "@/features/plan/types/marketingPlan";
import { contentStrategyChannelBadgeClass } from "@/features/plan/ui/contentStrategy/contentStrategyChannelBadgeClass";
import { contentStrategyChannelBadgeClass } from "@/features/plan/ui/contentStrategy/contentStrategyChannelBadgeClasses";
type ContentStrategyTypesTabProps = {
rows: ContentTypeRow[];

View File

@ -1,6 +1,6 @@
import CalendarIcon from "@/assets/report/calendar.svg?react";
import GlobeIcon from "@/assets/report/globe.svg?react";
import { PLAN_HEADER_META_CHIP_CLASS } from "@/features/plan/ui/header/planHeaderSectionStyles";
import { PLAN_HEADER_META_CHIP_CLASS } from "@/features/plan/ui/header/planHeaderSectionClasses";
type PlanHeaderMetaChipsProps = {
date: string;

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { useMainSubNav } from "@/layouts/MainSubNavLayout";
import type { SubNavItem } from "@/layouts/SubNav";
import { REPORT_SECTIONS } from "@/features/report/constants/report_sections";
import { REPORT_SECTIONS } from "@/features/report/config/reportSections";
export function useReportSubNav() {
const { setSubNav } = useMainSubNav();

View File

@ -1,4 +1,4 @@
import type { OtherChannelsReport } from "@/types/otherChannels";
import type { OtherChannelsReport } from "@/features/report/types/otherChannels";
/** DEMO `mockReport` 기타 채널 + 웹사이트 진단 */
export const MOCK_OTHER_CHANNELS_REPORT: OtherChannelsReport = {

View File

@ -1,5 +1,5 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_CHANNEL_SCORES } from "@/features/report/constants/mock_channel_scores";
import { MOCK_CHANNEL_SCORES } from "@/features/report/mocks/channelScores";
import type { ChannelScore } from "@/features/report/types/channelScore";
import { ChannelScoreGrid } from "@/features/report/ui/channels/ChannelScoreGrid";

View File

@ -1,5 +1,5 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_CLINIC_SNAPSHOT } from "@/features/report/constants/mock_clinic_snapshot";
import { MOCK_CLINIC_SNAPSHOT } from "@/features/report/mocks/clinicSnapshot";
import type { ClinicSnapshot } from "@/features/report/types/clinicSnapshot";
import { ClinicCertificationsBlock } from "@/features/report/ui/clinic/ClinicCertificationsBlock";
import { ClinicInfoStatGrid } from "@/features/report/ui/clinic/ClinicInfoStatGrid";

View File

@ -1,5 +1,5 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_PROBLEM_DIAGNOSIS } from "@/features/report/constants/mock_problem_diagnosis";
import { MOCK_PROBLEM_DIAGNOSIS } from "@/features/report/mocks/problemDiagnosis";
import type { DiagnosisItem } from "@/features/report/types/diagnosis";
import { ProblemDiagnosisCard } from "@/features/report/ui/diagnosis/ProblemDiagnosisCard";

View File

@ -1,9 +1,9 @@
import GlobeIcon from "@/assets/report/globe.svg?react";
import { BrandConsistencyMap } from "@/components/brand/BrandConsistencyMap";
import { DiagnosisRow } from "@/components/diagnosis/DiagnosisRow";
import { DiagnosisRow } from "@/features/report/ui/diagnosis/DiagnosisRow";
import { ConsolidationCallout } from "@/components/panel/ConsolidationCallout";
import { PageSection } from "@/components/section/PageSection";
import { MOCK_FACEBOOK_AUDIT } from "@/features/report/constants/mock_facebook_audit";
import { MOCK_FACEBOOK_AUDIT } from "@/features/report/mocks/facebookAudit";
import type { FacebookAudit } from "@/features/report/types/facebookAudit";
import { FacebookPageCard } from "@/features/report/ui/facebook/FacebookPageCard";

View File

@ -1,6 +1,6 @@
import { DiagnosisRow } from "@/components/diagnosis/DiagnosisRow";
import { DiagnosisRow } from "@/features/report/ui/diagnosis/DiagnosisRow";
import { PageSection } from "@/components/section/PageSection";
import { MOCK_INSTAGRAM_AUDIT } from "@/features/report/constants/mock_instagram_audit";
import { MOCK_INSTAGRAM_AUDIT } from "@/features/report/mocks/instagramAudit";
import type { InstagramAudit } from "@/features/report/types/instagramAudit";
import { InstagramAccountCard } from "@/features/report/ui/instagram/InstagramAccountCard";

View File

@ -1,5 +1,5 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_KPI_METRICS } from "@/features/report/constants/mock_kpi";
import { MOCK_KPI_METRICS } from "@/features/report/mocks/kpi";
import type { KpiMetric } from "@/features/report/types/kpiDashboard";
import { KpiMetricsTable } from "@/features/report/ui/kpi/KpiMetricsTable";
import { KpiTransformationCtaCard } from "@/features/report/ui/kpi/KpiTransformationCtaCard";

View File

@ -1,6 +1,6 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_OTHER_CHANNELS_REPORT } from "@/features/report/constants/mock_other_channels";
import type { OtherChannelsReport } from "@/types/otherChannels";
import { MOCK_OTHER_CHANNELS_REPORT } from "@/features/report/mocks/otherChannels";
import type { OtherChannelsReport } from "@/features/report/types/otherChannels";
import { OtherChannelsList } from "@/features/report/ui/otherChannels/OtherChannelsList";
import { WebsiteTechAuditBlock } from "@/features/report/ui/otherChannels/WebsiteTechAuditBlock";

View File

@ -1,9 +1,9 @@
import { MOCK_REPORT_OVERVIEW } from "@/features/report/constants/mock_report_overview";
import { MOCK_REPORT_OVERVIEW } from "@/features/report/mocks/reportOverview";
import type { ReportOverviewData } from "@/features/report/types/reportOverview";
import { OverviewHeroBlobs } from "@/features/report/ui/overview/OverviewHeroBlobs";
import { OverviewHeroColumn } from "@/features/report/ui/overview/OverviewHeroColumn";
import { OverviewScorePanel } from "@/features/report/ui/overview/OverviewScorePanel";
import { OVERVIEW_SECTION_BG_CLASS } from "@/features/report/ui/overview/overviewSectionStyles";
import { OVERVIEW_SECTION_BG_CLASS } from "@/features/report/ui/overview/overviewSectionClasses";
type ReportOverviewSectionProps = {
data?: ReportOverviewData;

View File

@ -1,40 +0,0 @@
import { useParams } from "react-router-dom";
import { useReportSubNav } from "@/features/report/hooks/useReportSubNav";
import { ReportChannelsSection } from "@/features/report/ui/ReportChannelsSection";
import { ReportClinicSection } from "@/features/report/ui/ReportClinicSection";
import { ReportDiagnosisSection } from "@/features/report/ui/ReportDiagnosisSection";
import { ReportFacebookSection } from "@/features/report/ui/ReportFacebookSection";
import { ReportInstagramSection } from "@/features/report/ui/ReportInstagramSection";
import { ReportKpiSection } from "@/features/report/ui/ReportKpiSection";
import { ReportOtherChannelsSection } from "@/features/report/ui/ReportOtherChannelsSection";
import { ReportOverviewSection } from "@/features/report/ui/ReportOverviewSection";
import { ReportRoadmapSection } from "@/features/report/ui/ReportRoadmapSection";
import { ReportTransformationSection } from "@/features/report/ui/ReportTransformationSection";
import { ReportYouTubeSection } from "@/features/report/ui/ReportYouTubeSection";
export function ReportPage() {
const { id } = useParams<{ id: string }>();
useReportSubNav();
return (
<div data-report-content>
<p className="body-14 text-neutral-60 px-6 max-w-7xl mx-auto py-2">
Report ID: <span className="text-navy-900 font-medium">{id}</span>
</p>
<div className="divide-y divide-neutral-20">
<ReportOverviewSection />
<ReportClinicSection />
<ReportChannelsSection />
<ReportYouTubeSection />
<ReportInstagramSection />
<ReportFacebookSection />
<ReportOtherChannelsSection />
<ReportDiagnosisSection />
<ReportTransformationSection />
<ReportRoadmapSection />
<ReportKpiSection />
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_ROADMAP } from "@/features/report/constants/mock_roadmap";
import { MOCK_ROADMAP } from "@/features/report/mocks/roadmap";
import type { RoadmapMonth } from "@/features/report/types/roadmap";
import { RoadmapMonthsGrid } from "@/features/report/ui/roadmap/RoadmapMonthsGrid";

View File

@ -1,5 +1,5 @@
import { PageSection } from "@/components/section/PageSection";
import { MOCK_TRANSFORMATION } from "@/features/report/constants/mock_transformation";
import { MOCK_TRANSFORMATION } from "@/features/report/mocks/transformation";
import type { TransformationProposal } from "@/features/report/types/transformationProposal";
import { TransformationTabbedView } from "@/features/report/ui/transformation/TransformationTabbedView";

View File

@ -1,7 +1,7 @@
import { TagChipList } from "@/components/chip/TagChipList";
import { DiagnosisRow } from "@/components/diagnosis/DiagnosisRow";
import { DiagnosisRow } from "@/features/report/ui/diagnosis/DiagnosisRow";
import { PageSection } from "@/components/section/PageSection";
import { MOCK_YOUTUBE_AUDIT } from "@/features/report/constants/mock_youtube_audit";
import { MOCK_YOUTUBE_AUDIT } from "@/features/report/mocks/youtubeAudit";
import type { YouTubeAudit } from "@/features/report/types/youtubeAudit";
import { YouTubeChannelInfoCard } from "@/features/report/ui/youtube/YouTubeChannelInfoCard";
import { YouTubeMetricsGrid } from "@/features/report/ui/youtube/YouTubeMetricsGrid";

View File

@ -6,7 +6,7 @@ import CalendarIcon from "@/assets/report/calendar.svg?react";
import GlobeIcon from "@/assets/report/globe.svg?react";
import MapPinIcon from "@/assets/report/map-pin.svg?react";
import type { ClinicSnapshot } from "@/features/report/types/clinicSnapshot";
import { formatCompactNumber } from "@/lib/formatNumber";
import { formatCompactNumber } from "@/utils/formatNumber";
export type ClinicStatRow = {
label: string;

View File

@ -1,6 +1,6 @@
import AlertCircleIcon from "@/assets/icons/alert-circle.svg?react";
import type { DiagnosisItem } from "@/features/report/types/diagnosis";
import { problemDiagnosisSeverityDotClass } from "@/features/report/ui/diagnosis/severityDotClass";
import { problemDiagnosisSeverityDotClass } from "@/features/report/ui/diagnosis/severityDotClasses";
export type ProblemDiagnosisCardProps = {
item: DiagnosisItem;

View File

@ -8,9 +8,9 @@ import ImageIcon from "@/assets/report/image.svg?react";
import Link2Icon from "@/assets/report/link-2.svg?react";
import MessageCircleIcon from "@/assets/report/message-circle.svg?react";
import type { FacebookPage } from "@/features/report/types/facebookAudit";
import { facebookLangBadgeClass } from "@/features/report/ui/facebook/langBadgeClass";
import { formatCompactNumber } from "@/lib/formatNumber";
import { safeUrl } from "@/lib/safeUrl";
import { facebookLangBadgeClass } from "@/features/report/ui/facebook/langBadgeClasses";
import { formatCompactNumber } from "@/utils/formatNumber";
import { safeUrl } from "@/utils/safeUrl";
export type FacebookPageCardProps = {
page: FacebookPage;

View File

@ -3,9 +3,9 @@ import ChannelInstagramIcon from "@/assets/icons/channel-instagram.svg?react";
import ExternalLinkIcon from "@/assets/icons/external-link.svg?react";
import { TagChipList } from "@/components/chip/TagChipList";
import type { InstagramAccount } from "@/features/report/types/instagramAudit";
import { instagramLangBadgeClass } from "@/features/report/ui/instagram/langBadgeClass";
import { formatCompactNumber } from "@/lib/formatNumber";
import { safeUrl } from "@/lib/safeUrl";
import { instagramLangBadgeClass } from "@/features/report/ui/instagram/langBadgeClasses";
import { formatCompactNumber } from "@/utils/formatNumber";
import { safeUrl } from "@/utils/safeUrl";
export type InstagramAccountCardProps = {
account: InstagramAccount;

View File

@ -2,8 +2,8 @@ import ExternalLinkIcon from "@/assets/icons/external-link.svg?react";
import CheckCircleIcon from "@/assets/report/check-circle.svg?react";
import HelpCircleIcon from "@/assets/report/help-circle.svg?react";
import XCircleIcon from "@/assets/report/x-circle.svg?react";
import type { OtherChannelStatus } from "@/types/otherChannels";
import { safeUrl } from "@/lib/safeUrl";
import type { OtherChannelStatus } from "@/features/report/types/otherChannels";
import { safeUrl } from "@/utils/safeUrl";
export type OtherChannelRowProps = {
name: string;

View File

@ -1,5 +1,5 @@
import { OtherChannelRow } from "@/components/channel/OtherChannelRow";
import type { OtherChannel } from "@/types/otherChannels";
import { OtherChannelRow } from "@/features/report/ui/otherChannels/OtherChannelRow";
import type { OtherChannel } from "@/features/report/types/otherChannels";
export type OtherChannelsListProps = {
channels: OtherChannel[];

View File

@ -2,7 +2,7 @@ import AlertCircleIcon from "@/assets/icons/alert-circle.svg?react";
import CheckCircleIcon from "@/assets/report/check-circle.svg?react";
import GlobeIcon from "@/assets/report/globe.svg?react";
import { PixelInstallCard } from "@/components/card/PixelInstallCard";
import type { WebsiteAudit } from "@/types/otherChannels";
import type { WebsiteAudit } from "@/features/report/types/otherChannels";
export type WebsiteTechAuditBlockProps = {
website: WebsiteAudit;

View File

@ -1,7 +1,7 @@
import CalendarIcon from "@/assets/report/calendar.svg?react";
import GlobeIcon from "@/assets/report/globe.svg?react";
import MapPinIcon from "@/assets/report/map-pin.svg?react";
import { OVERVIEW_META_CHIP_CLASS } from "@/features/report/ui/overview/overviewSectionStyles";
import { OVERVIEW_META_CHIP_CLASS } from "@/features/report/ui/overview/overviewSectionClasses";
export type OverviewMetaChipsProps = {
date: string;

View File

@ -1,5 +1,5 @@
import type { NewChannelProposal } from "@/features/report/types/transformationProposal";
import { newChannelPriorityClass } from "@/features/report/ui/transformation/newChannelPriorityClass";
import { newChannelPriorityClass } from "@/features/report/ui/transformation/newChannelPriorityClasses";
export type NewChannelProposalsTableProps = {
rows: NewChannelProposal[];

View File

@ -1,5 +1,5 @@
import { useState } from "react";
import { ComparisonRow } from "@/components/compare/ComparisonRow";
import { ComparisonRow } from "@/features/report/ui/transformation/ComparisonRow";
import type { TransformationProposal } from "@/features/report/types/transformationProposal";
import { NewChannelProposalsTable } from "@/features/report/ui/transformation/NewChannelProposalsTable";
import { PlatformStrategyCard } from "@/features/report/ui/transformation/PlatformStrategyCard";

View File

@ -1,7 +1,7 @@
import ChannelYoutubeIcon from "@/assets/icons/channel-youtube.svg?react";
import ExternalLinkIcon from "@/assets/icons/external-link.svg?react";
import type { YouTubeAudit } from "@/features/report/types/youtubeAudit";
import { safeUrl } from "@/lib/safeUrl";
import { safeUrl } from "@/utils/safeUrl";
export type YouTubeChannelInfoCardProps = {
data: YouTubeAudit;

View File

@ -4,7 +4,7 @@ import VideoIcon from "@/assets/icons/video.svg?react";
import UsersIcon from "@/assets/icons/users.svg?react";
import { MetricCard } from "@/components/card/MetricCard";
import type { YouTubeAudit } from "@/features/report/types/youtubeAudit";
import { formatCompactNumber } from "@/lib/formatNumber";
import { formatCompactNumber } from "@/utils/formatNumber";
export type YouTubeMetricsGridProps = {
data: YouTubeAudit;

View File

@ -68,7 +68,7 @@ export function PageNavigator() {
{/* 이전 페이지 */}
<button
type="button"
onClick={() => prev && navigate(prev.navigatePath)}
onClick={() => { if (prev) { navigate(prev.navigatePath); window.scrollTo({ top: 0, behavior: "smooth" }); } }}
disabled={!prev}
aria-label={prev ? `이전: ${prev.label}` : "이전 페이지 없음"}
className="body-14-medium flex items-center gap-2 px-3 py-2 rounded-full text-neutral-70 transition-all hover:bg-neutral-20 hover:text-navy-900 disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
@ -83,7 +83,7 @@ export function PageNavigator() {
<button
key={page.id}
type="button"
onClick={() => navigate(page.navigatePath)}
onClick={() => { navigate(page.navigatePath); window.scrollTo({ top: 0, behavior: "smooth" }); }}
title={page.label}
aria-label={page.label}
className={`rounded-full transition-all cursor-pointer ${
@ -98,7 +98,7 @@ export function PageNavigator() {
{/* 다음 페이지 */}
<button
type="button"
onClick={() => next && navigate(next.navigatePath)}
onClick={() => { if (next) { navigate(next.navigatePath); window.scrollTo({ top: 0, behavior: "smooth" }); } }}
disabled={!next}
aria-label={next ? `다음: ${next.label}` : "다음 페이지 없음"}
className="body-14-medium flex items-center gap-2 px-3 py-2 rounded-full text-neutral-70 transition-all hover:bg-neutral-20 hover:text-navy-900 disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"

View File

@ -1,5 +1,44 @@
import { MarketingPlanPage } from "@/features/plan/ui/MarketingPlanPage";
import { useCurrentMarketingPlan } from "@/features/plan/hooks/useCurrentMarketingPlan";
import { usePlanSubNav } from "@/features/plan/hooks/usePlanSubNav";
import { PlanHeaderSection } from "@/features/plan/ui/PlanHeaderSection";
import { PlanBrandingGuideSection } from "@/features/plan/ui/PlanBrandingGuideSection";
import { PlanChannelStrategySection } from "@/features/plan/ui/PlanChannelStrategySection";
import { PlanContentStrategySection } from "@/features/plan/ui/PlanContentStrategySection";
import { PlanContentCalendarSection } from "@/features/plan/ui/PlanContentCalendarSection";
import { PlanAssetCollectionSection } from "@/features/plan/ui/PlanAssetCollectionSection";
import { PlanMyAssetUploadSection } from "@/features/plan/ui/PlanMyAssetUploadSection";
import { PlanCtaSection } from "@/features/plan/ui/PlanCtaSection";
export function PlanPage() {
return <MarketingPlanPage />;
const { data, error } = useCurrentMarketingPlan();
usePlanSubNav();
if (error || !data) {
return (
<div className="min-h-[50vh] flex items-center justify-center px-6">
<div className="text-center max-w-md">
<p className="title-16 text-[var(--color-status-critical-text)] mb-2 break-keep">
</p>
<p className="body-14 text-neutral-60 break-keep">
{error ?? "마케팅 플랜을 찾을 수 없습니다."}
</p>
</div>
</div>
);
}
return (
<div data-plan-content data-plan-id={data.id}>
<PlanHeaderSection />
<PlanBrandingGuideSection />
<PlanChannelStrategySection />
<PlanContentStrategySection />
<PlanContentCalendarSection />
<PlanAssetCollectionSection />
<PlanMyAssetUploadSection />
<PlanCtaSection />
</div>
);
}

View File

@ -1,5 +1,34 @@
import { ReportPage as ReportFeaturePage } from "@/features/report/ui/ReportPage";
import { useReportSubNav } from "@/features/report/hooks/useReportSubNav";
import { ReportOverviewSection } from "@/features/report/ui/ReportOverviewSection";
import { ReportClinicSection } from "@/features/report/ui/ReportClinicSection";
import { ReportChannelsSection } from "@/features/report/ui/ReportChannelsSection";
import { ReportYouTubeSection } from "@/features/report/ui/ReportYouTubeSection";
import { ReportInstagramSection } from "@/features/report/ui/ReportInstagramSection";
import { ReportFacebookSection } from "@/features/report/ui/ReportFacebookSection";
import { ReportOtherChannelsSection } from "@/features/report/ui/ReportOtherChannelsSection";
import { ReportDiagnosisSection } from "@/features/report/ui/ReportDiagnosisSection";
import { ReportTransformationSection } from "@/features/report/ui/ReportTransformationSection";
import { ReportRoadmapSection } from "@/features/report/ui/ReportRoadmapSection";
import { ReportKpiSection } from "@/features/report/ui/ReportKpiSection";
export function ReportPage() {
return <ReportFeaturePage />;
useReportSubNav();
return (
<div data-report-content>
<div className="divide-y divide-neutral-20">
<ReportOverviewSection />
<ReportClinicSection />
<ReportChannelsSection />
<ReportYouTubeSection />
<ReportInstagramSection />
<ReportFacebookSection />
<ReportOtherChannelsSection />
<ReportDiagnosisSection />
<ReportTransformationSection />
<ReportRoadmapSection />
<ReportKpiSection />
</div>
</div>
);
}

2
src/services/index.ts Normal file
View File

@ -0,0 +1,2 @@
// 공통 API 클라이언트 설정 (Axios 인스턴스, 인터셉터 등)
// TODO: Phase 1 백엔드 연동 시 구현

2
src/store/index.ts Normal file
View File

@ -0,0 +1,2 @@
// 전역 상태 관리 (사용자 세션, 테마 등)
// TODO: Phase 1 백엔드 연동 시 구현