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 { 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 (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
|
@ -19,16 +23,24 @@ function App() {
|
|||
path="/"
|
||||
element={
|
||||
<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">
|
||||
fromis_9 Frontend Refactoring
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Phase 1 완료 - 프로젝트 셋업
|
||||
Phase 2 완료 - 유틸리티 및 상수
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
<p className={cn("text-sm", isMobile ? "text-blue-500" : "text-green-500")}>
|
||||
디바이스: {isMobile ? "모바일" : "PC"}
|
||||
</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>
|
||||
}
|
||||
|
|
|
|||
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