feat(frontend): Phase 2 - 유틸리티 및 상수 정의
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
4ec368c936
commit
dc63a91f4f
8 changed files with 368 additions and 9 deletions
|
|
@ -1,17 +1,21 @@
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
|
import { cn, getTodayKST, formatFullDate, formatXDateTime } from "@/utils";
|
||||||
|
import { CATEGORY_NAMES, SOCIAL_LINKS } from "@/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 프로미스나인 팬사이트 메인 앱
|
* 프로미스나인 팬사이트 메인 앱
|
||||||
*
|
*
|
||||||
* Phase 1: 프로젝트 셋업 완료
|
* Phase 2: 유틸리티 및 상수 완료
|
||||||
* - 기본 구조 및 설정 파일 생성
|
* - constants/index.js: 상수 정의 (카테고리, SNS 링크 등)
|
||||||
* - React Query, React Router 설정
|
* - utils/cn.js: className 유틸리티 (clsx 기반)
|
||||||
* - Tailwind CSS 설정
|
* - utils/date.js: 날짜 관련 유틸리티
|
||||||
*
|
* - utils/format.js: 포맷팅 유틸리티
|
||||||
* 다음 단계에서 유틸리티, 스토어, API, 컴포넌트들이 추가될 예정
|
* - utils/index.js: 통합 export
|
||||||
*/
|
*/
|
||||||
function App() {
|
function App() {
|
||||||
|
const today = getTodayKST();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
@ -19,16 +23,24 @@ function App() {
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
<div className="text-center">
|
<div className="text-center space-y-4">
|
||||||
<h1 className="text-2xl font-bold text-primary mb-2">
|
<h1 className="text-2xl font-bold text-primary mb-2">
|
||||||
fromis_9 Frontend Refactoring
|
fromis_9 Frontend Refactoring
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Phase 1 완료 - 프로젝트 셋업
|
Phase 2 완료 - 유틸리티 및 상수
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
<p className={cn("text-sm", isMobile ? "text-blue-500" : "text-green-500")}>
|
||||||
디바이스: {isMobile ? "모바일" : "PC"}
|
디바이스: {isMobile ? "모바일" : "PC"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-white rounded-lg shadow text-left text-sm space-y-2">
|
||||||
|
<p><strong>오늘 날짜:</strong> {today}</p>
|
||||||
|
<p><strong>포맷된 날짜:</strong> {formatFullDate(today)}</p>
|
||||||
|
<p><strong>X 스타일:</strong> {formatXDateTime(today, "19:00")}</p>
|
||||||
|
<p><strong>카테고리:</strong> {Object.values(CATEGORY_NAMES).join(", ")}</p>
|
||||||
|
<p><strong>SNS 개수:</strong> {Object.keys(SOCIAL_LINKS).length}개</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
frontend-temp/src/constants/index.js
Normal file
47
frontend-temp/src/constants/index.js
Normal file
|
|
@ -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';
|
||||||
16
frontend-temp/src/utils/cn.js
Normal file
16
frontend-temp/src/utils/cn.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
156
frontend-temp/src/utils/date.js
Normal file
156
frontend-temp/src/utils/date.js
Normal file
|
|
@ -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 };
|
||||||
94
frontend-temp/src/utils/format.js
Normal file
94
frontend-temp/src/utils/format.js
Normal file
|
|
@ -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)}...`;
|
||||||
|
};
|
||||||
34
frontend-temp/src/utils/index.js
Normal file
34
frontend-temp/src/utils/index.js
Normal file
|
|
@ -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';
|
||||||
Loading…
Add table
Reference in a new issue