From dc63a91f4fef15ba47334df34259cd21c52c80c1 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 17:07:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=202=20-=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=EB=B0=8F=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - constants/index.js: 카테고리, SNS 링크, 앨범 타입, 타임존 상수 - utils/cn.js: clsx 기반 className 유틸리티 - utils/date.js: dayjs 기반 날짜 유틸리티 (KST) - utils/format.js: HTML 디코딩, 숫자/시간 포맷팅 - utils/index.js: 통합 export Co-Authored-By: Claude Opus 4.5 --- frontend-temp/src/App.jsx | 30 ++++-- frontend-temp/src/constants/.gitkeep | 0 frontend-temp/src/constants/index.js | 47 ++++++++ frontend-temp/src/utils/.gitkeep | 0 frontend-temp/src/utils/cn.js | 16 +++ frontend-temp/src/utils/date.js | 156 +++++++++++++++++++++++++++ frontend-temp/src/utils/format.js | 94 ++++++++++++++++ frontend-temp/src/utils/index.js | 34 ++++++ 8 files changed, 368 insertions(+), 9 deletions(-) delete mode 100644 frontend-temp/src/constants/.gitkeep create mode 100644 frontend-temp/src/constants/index.js delete mode 100644 frontend-temp/src/utils/.gitkeep create mode 100644 frontend-temp/src/utils/cn.js create mode 100644 frontend-temp/src/utils/date.js create mode 100644 frontend-temp/src/utils/format.js create mode 100644 frontend-temp/src/utils/index.js diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx index df7156e..c95df24 100644 --- a/frontend-temp/src/App.jsx +++ b/frontend-temp/src/App.jsx @@ -1,17 +1,21 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import { isMobile } from "react-device-detect"; +import { cn, getTodayKST, formatFullDate, formatXDateTime } from "@/utils"; +import { CATEGORY_NAMES, SOCIAL_LINKS } from "@/constants"; /** * 프로미스나인 팬사이트 메인 앱 * - * Phase 1: 프로젝트 셋업 완료 - * - 기본 구조 및 설정 파일 생성 - * - React Query, React Router 설정 - * - Tailwind CSS 설정 - * - * 다음 단계에서 유틸리티, 스토어, API, 컴포넌트들이 추가될 예정 + * Phase 2: 유틸리티 및 상수 완료 + * - constants/index.js: 상수 정의 (카테고리, SNS 링크 등) + * - utils/cn.js: className 유틸리티 (clsx 기반) + * - utils/date.js: 날짜 관련 유틸리티 + * - utils/format.js: 포맷팅 유틸리티 + * - utils/index.js: 통합 export */ function App() { + const today = getTodayKST(); + return ( @@ -19,16 +23,24 @@ function App() { path="/" element={
-
+

fromis_9 Frontend Refactoring

- Phase 1 완료 - 프로젝트 셋업 + Phase 2 완료 - 유틸리티 및 상수

-

+

디바이스: {isMobile ? "모바일" : "PC"}

+ +
+

오늘 날짜: {today}

+

포맷된 날짜: {formatFullDate(today)}

+

X 스타일: {formatXDateTime(today, "19:00")}

+

카테고리: {Object.values(CATEGORY_NAMES).join(", ")}

+

SNS 개수: {Object.keys(SOCIAL_LINKS).length}개

+
} diff --git a/frontend-temp/src/constants/.gitkeep b/frontend-temp/src/constants/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/frontend-temp/src/constants/index.js b/frontend-temp/src/constants/index.js new file mode 100644 index 0000000..e1a043a --- /dev/null +++ b/frontend-temp/src/constants/index.js @@ -0,0 +1,47 @@ +/** + * 상수 정의 + */ + +/** 카테고리 ID */ +export const CATEGORY_ID = { + DEFAULT: 1, + YOUTUBE: 2, + X: 3, +}; + +/** 카테고리 이름 매핑 */ +export const CATEGORY_NAMES = { + [CATEGORY_ID.DEFAULT]: '기본', + [CATEGORY_ID.YOUTUBE]: 'YouTube', + [CATEGORY_ID.X]: 'X (Twitter)', +}; + +/** 공식 SNS 링크 */ +export const SOCIAL_LINKS = { + youtube: 'https://www.youtube.com/@fromis9_official', + instagram: 'https://www.instagram.com/officialfromis_9', + twitter: 'https://twitter.com/realfromis_9', + tiktok: 'https://www.tiktok.com/@officialfromis_9', + fancafe: 'https://cafe.daum.net/officialfromis9', +}; + +/** 앨범 타입 */ +export const ALBUM_TYPES = { + FULL: '정규', + MINI: '미니', + SINGLE: '싱글', + DIGITAL: '디지털', + OST: 'OST', +}; + +/** 타임존 */ +export const TIMEZONE = 'Asia/Seoul'; + +/** 요일 이름 */ +export const WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토']; + +/** 기본 페이지 크기 */ +export const DEFAULT_PAGE_SIZE = 20; + +/** API 기본 URL */ +export const API_BASE_URL = '/api'; diff --git a/frontend-temp/src/utils/.gitkeep b/frontend-temp/src/utils/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/frontend-temp/src/utils/cn.js b/frontend-temp/src/utils/cn.js new file mode 100644 index 0000000..dcc3536 --- /dev/null +++ b/frontend-temp/src/utils/cn.js @@ -0,0 +1,16 @@ +import { clsx } from 'clsx'; + +/** + * className 유틸리티 + * clsx를 래핑하여 조건부 클래스 조합을 쉽게 처리 + * + * @example + * cn('base-class', isActive && 'active', { 'error': hasError }) + * // => 'base-class active error' (조건에 따라) + * + * @param {...(string|object|array|boolean|null|undefined)} inputs - 클래스 입력 + * @returns {string} 조합된 클래스 문자열 + */ +export function cn(...inputs) { + return clsx(inputs); +} diff --git a/frontend-temp/src/utils/date.js b/frontend-temp/src/utils/date.js new file mode 100644 index 0000000..ce32a4f --- /dev/null +++ b/frontend-temp/src/utils/date.js @@ -0,0 +1,156 @@ +/** + * 날짜 관련 유틸리티 함수 + * dayjs를 사용하여 KST(한국 표준시) 기준으로 날짜 처리 + */ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import { TIMEZONE, WEEKDAYS } from '@/constants'; + +// 플러그인 확장 +dayjs.extend(utc); +dayjs.extend(timezone); + +/** + * KST 기준 오늘 날짜 (YYYY-MM-DD) + * @returns {string} 오늘 날짜 문자열 + */ +export const getTodayKST = () => { + return dayjs().tz(TIMEZONE).format('YYYY-MM-DD'); +}; + +/** + * KST 기준 현재 시각 + * @returns {dayjs.Dayjs} dayjs 객체 + */ +export const nowKST = () => { + return dayjs().tz(TIMEZONE); +}; + +/** + * 날짜 문자열 포맷팅 + * @param {string|Date} date - 날짜 + * @param {string} format - 포맷 (기본: 'YYYY-MM-DD') + * @returns {string} 포맷된 날짜 문자열 + */ +export const formatDate = (date, format = 'YYYY-MM-DD') => { + if (!date) return ''; + return dayjs(date).tz(TIMEZONE).format(format); +}; + +/** + * 날짜에서 년, 월, 일, 요일 추출 + * @param {string|Date} date - 날짜 + * @returns {{ year: number, month: number, day: number, weekday: string }} + */ +export const parseDateKST = (date) => { + const d = dayjs(date).tz(TIMEZONE); + return { + year: d.year(), + month: d.month() + 1, + day: d.date(), + weekday: WEEKDAYS[d.day()], + }; +}; + +/** + * 두 날짜 비교 (같은 날인지) + * @param {string|Date} date1 + * @param {string|Date} date2 + * @returns {boolean} + */ +export const isSameDay = (date1, date2) => { + return ( + dayjs(date1).tz(TIMEZONE).format('YYYY-MM-DD') === + dayjs(date2).tz(TIMEZONE).format('YYYY-MM-DD') + ); +}; + +/** + * 날짜가 오늘인지 확인 + * @param {string|Date} date + * @returns {boolean} + */ +export const isToday = (date) => { + return isSameDay(date, dayjs()); +}; + +/** + * 날짜가 과거인지 확인 + * @param {string|Date} date + * @returns {boolean} + */ +export const isPast = (date) => { + return dayjs(date).tz(TIMEZONE).isBefore(dayjs().tz(TIMEZONE), 'day'); +}; + +/** + * 날짜가 미래인지 확인 + * @param {string|Date} date + * @returns {boolean} + */ +export const isFuture = (date) => { + return dayjs(date).tz(TIMEZONE).isAfter(dayjs().tz(TIMEZONE), 'day'); +}; + +/** + * 전체 날짜 포맷 (YYYY. M. D. (요일)) + * @param {string|Date} date - 날짜 + * @returns {string} 포맷된 문자열 + */ +export const formatFullDate = (date) => { + if (!date) return ''; + const d = dayjs(date).tz(TIMEZONE); + return `${d.year()}. ${d.month() + 1}. ${d.date()}. (${WEEKDAYS[d.day()]})`; +}; + +/** + * X(트위터) 스타일 날짜/시간 포맷팅 + * @param {string} date - 날짜 문자열 (YYYY-MM-DD) + * @param {string} [time] - 시간 문자열 (HH:mm 또는 HH:mm:ss) + * @returns {string} "오후 7:00 · 2026년 1월 18일" 또는 "2026년 1월 18일" + */ +export const formatXDateTime = (date, time) => { + if (!date) return ''; + + const d = dayjs(date).tz(TIMEZONE); + const datePart = `${d.year()}년 ${d.month() + 1}월 ${d.date()}일`; + + if (time) { + const [hours, minutes] = time.split(':').map(Number); + const period = hours < 12 ? '오전' : '오후'; + const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + return `${period} ${hour12}:${String(minutes).padStart(2, '0')} · ${datePart}`; + } + + return datePart; +}; + +/** + * datetime 문자열에서 date 추출 + * @param {string} datetime - "YYYY-MM-DD HH:mm" 또는 "YYYY-MM-DD" + * @returns {string} "YYYY-MM-DD" + */ +export const extractDate = (datetime) => { + if (!datetime) return ''; + return datetime.split(' ')[0].split('T')[0]; +}; + +/** + * datetime 문자열에서 time 추출 + * @param {string} datetime - "YYYY-MM-DD HH:mm" 또는 "YYYY-MM-DDTHH:mm" + * @returns {string|null} "HH:mm" 또는 null + */ +export const extractTime = (datetime) => { + if (!datetime) return null; + if (datetime.includes(' ')) { + return datetime.split(' ')[1]?.slice(0, 5) || null; + } + if (datetime.includes('T')) { + return datetime.split('T')[1]?.slice(0, 5) || null; + } + return null; +}; + +// dayjs 인스턴스도 export (고급 사용용) +export { dayjs }; diff --git a/frontend-temp/src/utils/format.js b/frontend-temp/src/utils/format.js new file mode 100644 index 0000000..bba1881 --- /dev/null +++ b/frontend-temp/src/utils/format.js @@ -0,0 +1,94 @@ +/** + * 포맷팅 관련 유틸리티 함수 + */ + +/** + * HTML 엔티티 디코딩 + * @param {string} text - HTML 엔티티가 포함된 텍스트 + * @returns {string} 디코딩된 텍스트 + */ +export const decodeHtmlEntities = (text) => { + if (!text) return ''; + const textarea = document.createElement('textarea'); + textarea.innerHTML = text; + return textarea.value; +}; + +/** + * 시간 문자열 포맷팅 (HH:mm 형식으로) + * @param {string} time - 시간 문자열 (HH:mm:ss 또는 HH:mm) + * @returns {string|null} 포맷된 시간 또는 null + */ +export const formatTime = (time) => { + if (!time) return null; + return time.slice(0, 5); +}; + +/** + * 숫자에 천 단위 콤마 추가 + * @param {number} num - 숫자 + * @returns {string} 콤마가 추가된 문자열 + */ +export const formatNumber = (num) => { + if (num === null || num === undefined) return '0'; + return num.toLocaleString('ko-KR'); +}; + +/** + * 조회수 포맷팅 (1만 이상이면 '만' 단위로) + * @param {number} count - 조회수 + * @returns {string} 포맷된 조회수 + */ +export const formatViewCount = (count) => { + if (!count) return '0'; + if (count >= 10000) { + return `${(count / 10000).toFixed(1)}만`; + } + return formatNumber(count); +}; + +/** + * 파일 크기 포맷팅 + * @param {number} bytes - 바이트 단위 크기 + * @returns {string} 포맷된 크기 (예: "1.5 MB") + */ +export const formatFileSize = (bytes) => { + if (!bytes) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return `${size.toFixed(1)} ${units[unitIndex]}`; +}; + +/** + * 재생 시간 포맷팅 (초 -> MM:SS 또는 HH:MM:SS) + * @param {number} seconds - 초 단위 시간 + * @returns {string} 포맷된 시간 + */ +export const formatDuration = (seconds) => { + if (!seconds) return '0:00'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; + } + return `${minutes}:${String(secs).padStart(2, '0')}`; +}; + +/** + * 텍스트 말줄임 처리 + * @param {string} text - 원본 텍스트 + * @param {number} maxLength - 최대 길이 + * @returns {string} 말줄임 처리된 텍스트 + */ +export const truncateText = (text, maxLength) => { + if (!text) return ''; + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}...`; +}; diff --git a/frontend-temp/src/utils/index.js b/frontend-temp/src/utils/index.js new file mode 100644 index 0000000..5fedaa1 --- /dev/null +++ b/frontend-temp/src/utils/index.js @@ -0,0 +1,34 @@ +/** + * 유틸리티 함수 통합 export + */ + +// className 유틸리티 +export { cn } from './cn'; + +// 날짜 관련 +export { + getTodayKST, + nowKST, + formatDate, + parseDateKST, + isSameDay, + isToday, + isPast, + isFuture, + formatFullDate, + formatXDateTime, + extractDate, + extractTime, + dayjs, +} from './date'; + +// 포맷팅 관련 +export { + decodeHtmlEntities, + formatTime, + formatNumber, + formatViewCount, + formatFileSize, + formatDuration, + truncateText, +} from './format';