소개
기존의 검색엔진에 존재하는 달 모양 사이트의 UI/UX가 현대적이지 못하고, 광고가 많아서 친구의 아이디어 추천에 따라 만들게 되었습니다.
홈페이지 : https://lunar.wisoft.io/
진행 방법
Cursor AI 개발 진행했습니다.
초기 프롬프트
달의 모양을 확인하는 사이트를 현대적인 디자인으로 구성하기 위해서 기능 정의서를 작성해 주세요.
# Lunar Calendar (달력) 기능 정의서
## 1. 프로젝트 개요
### 1.1. 프로젝트 목적
- 달의 위상과 천문학적 데이터를 실시간으로 제공하는 웹 서비스 구축
- 사용자 위치 기반의 정확한 달 관련 정보 제공
- 직관적이고 아름다운 사용자 인터페이스 제공
### 1.2. 기술 스택
- Frontend: React.js, TypeScript, TailwindCSS
- Backend: Node.js, Express
- Infrastructure: Docker
## 2. 핵심 기능 상세
### 2.1. 오늘의 달 정보 (Today's Moon)
#### 필수 기능
- 현재 달의 위상 표시 (예: 신월, 초승달, 상현달, 상현망간의 달, 보름달, 하현망간의 달, 하현달, 그믐달)
- 달령(Moon Age) 및 밝기 백분율 계산
- 실시간 월출/월몰 시간 표시
- 한국천문연구원_출몰시각 정보 API 사용
- API KEY : **************
```
날짜와 지역을 기준으로 일출, 일남중, 일몰, 월출, 월남중, 월몰, 시민박명(아침), 시민박명(저녁), 항해박명(아침), 항해박명(저녁), 천문박명(아침), 천문박명(저녁) 등의 정보를 제공한다.
활용승인 절차 개발단계 : 자동승인 / 운영단계 : 자동승인
신청가능 트래픽 개발계정 : 10,000 / 운영계정 : 활용사례 등록시 신청하면 트래픽 증가 가능
요청주소 http://apis.data.go.kr/B090041/openapi/service/RiseSetInfoService/getAreaRiseSetInfo
서비스URL http://apis.data.go.kr/B090041/openapi/service/RiseSetInfoService
```
```
주소요청변수(Request Parameter)
항목명(국문) 항목명(영문) 항목크기 항목구분 샘플데이터 항목설명
날짜 locdate 8 필수 20150101 날짜
지역 location 10 필수 서울 지역
출력결과(Response Element)
항목명(국문) 항목명(영문) 항목크기 항목구분 샘플데이터 항목설명
날짜 locdate 8 필수 20150101 날짜
지역 locatiaon 10 필수 서울 지역
경도 longitude 5 필수 12800 경도
위도 latitude 4 필수 3700 위도
일출 sunrise 6 필수 0746 일출
일중 suntransit 6 필수 1235 일중
일몰 sunset 6 필수 1723 일몰
월출 moonrise 6 필수 1428 월출
월중 moontransit 6 필수 2132 월중
월몰 moonset 6 필수 0340 월몰
시민박명(아침) civilm 6 필수 0717 시민박명(아침)
시민박명(저녁) civile 6 필수 1753 시민박명(저녁)
항해박명(아침) nautm 6 필수 0644 항해박명(아침)
항해박명(저녁) naute 6 필수 1825 항해박명(저녁)
천문박명(아침) astm 6 필수 0613 천문박명(아침)
천문박명(저녁) aste 6 필수 1857 천문박명(저녁)
페이지당항목수 numOfRows 필수 10
페이지 pageNo 필수 1
모든항목수 totalCount 필수 210114
```
```
날짜와 위치를 기준으로 일출, 일남중, 일몰, 월출, 월남중, 월몰, 시민박명(아침), 시민박명(저녁), 항해박명(아침), 항해박명(저녁), 천문박명(아침), 천문박명(저녁) 등의 정보를 제공한다.
활용승인 절차 개발단계 : 자동승인 / 운영단계 : 자동승인
신청가능 트래픽 개발계정 : 10,000 / 운영계정 : 활용사례 등록시 신청하면 트래픽 증가 가능
요청주소 http://apis.data.go.kr/B090041/openapi/service/RiseSetInfoService/getLCRiseSetInfo
서비스URL http://apis.data.go.kr/B090041/openapi/service/RiseSetInfoService
```
```
요청변수(Request Parameter)
항목명(국문) 항목명(영문) 항목크기 항목구분 샘플데이터 항목설명
날짜 locdate 8 필수 20170101 날짜(연월일)
경도 longitude 5 필수 12800 경도(도, 분형태)
위도 latitude 4 필수 3613 위도(도, 분형태)
10진수 여부 dnYn 1 필수 N 실수형태(129.xxx)일경우 Y, 도와 분(128도 00분)형태의 경우 N
출력결과(Response Element)
항목명(국문) 항목명(영문) 항목크기 항목구분 샘플데이터 항목설명
결과코드 resultCode 필수 00 00:성공
결과메시지 resultMag 필수 NORMAL SERVICE
날짜 locdate 8 필수 20170901 날짜
지역 location 10 필수 서울 지역명
경도 longitude 5 필수 12800 경도
경도(10진수) longitudeNum 18,7 필수 128.0000000 경도(10진수)
위도 latitude 4 필수 3613 위도
위도(10진수) latitudeNum 18,7 필수 36.2166660 위도(10진수)
일출 sunrise 6 필수 055906 일출시각
일중 suntransit 6 122806 일중시각
일몰 sunset 6 185632 일몰시각
월출 moonrise 6 222157 월출시각
월중 moontransit 6 042412 월중시각
월몰 moonset 6 110911 월몰시각
시민박명(아침) civilm 6 053247 시민박명(아침)시각
시민박명(저녁) civile 6 192247 시민박명(저녁)시각
항해박명(아침) nautm 6 050134 항해박명(아침)시각
항해박명(저녁) naute 6 195354 항해박명(저녁)시각
천문박명(아침) astm 6 042918 천문박명(아침)시각
천문박명(저녁) aste 6 필수 202602 천문박명(저녁)시각
페이지당 항목 수 numOfRows 필수 10
페이지수 pageNo 필수 1
모든항목수 totalCount 필수 16
```
- 달 사진은 public/moon_phase 폴더에 저장되어 있으므로 이를 활용하여 달 위상 시각화
- 파일 이름은 20190112--5.7.jpeg 형식으로 YYYYMMDD--달 나이.jpeg 형식으로 저장되어 있다.
- 프론트에서 달 위상을 위해 계산을 진행하고 파일을 참고하여 이에 맞는 달 사진을 사용하고, 없다면 가장 비슷한 사진을 사용한다.
#### 구현 세부사항
- 사용자의 IP를 기반으로 위치를 통해 천문 데이터를 받아온다.
- 사용자는 별도의 주소 입력을 통해 위치를 변경할 수 있다.
- 위치 변경 시 천문 데이터도 재계산된다.
#### 기능 요구사항
- 달의 위상 단계별 분류 및 표시
```
달의 나이와 밝기, 위상의 관계
달의 나이 (일) 밝기 비율 (%) 위상 설명
0 ~ 1 0% 신월 (New Moon) 달이 태양과 같은 방향에 있어 보이지 않음
1 ~ 7.4 1% ~ 49% 초승달 (Waxing Crescent) 달의 오른쪽 일부가 점점 밝아지는 상태
약 7.4 50% 상현달 (First Quarter) 달의 오른쪽 절반이 빛나는 상태
7.4 ~ 14.8 51% ~ 99% 상현망간의 달 (Waxing Gibbous) 보름달로 가는 과정에서 오른쪽이 더욱 밝아짐
약 14.8 100% 보름달 (Full Moon) 달의 전면이 완전히 빛나는 상태
14.8 ~ 22.1 51% ~ 99% 하현망간의 달 (Waning Gibbous) 보름달 이후 왼쪽이 점점 줄어드는 상태
약 22.1 50% 하현달 (Last Quarter) 달의 왼쪽 절반이 빛나는 상태
22.1 ~ 29.53 1% ~ 49% 그믐달 (Waning Crescent) 달의 왼쪽 일부가 점점 작아지는 상태
```
- 달 사진은 public/moon_phase 폴더의 이미지 활용
- 폴더 구조:
```
public/moon_phase/
├── 01_New_Moon/
├── 02_Waxing_Crescent/
├── 03_First_Quarter/
├── 04_Waxing_Gibbous/
├── 05_Full_Moon/
├── 06_Waning_Gibbous/
├── 07_Last_Quarter/
└── 08_Waning_Crescent/
```
- 파일명 형식: YYYYMMDD--달령.jpeg (예: 20190112--5.7.jpeg)
- 이미지 매칭 로직:
```typescript
interface MoonImage {
path: string;
date: string;
age: number;
phase: string;
}
// 달령에 따른 이미지 찾기
const findClosestMoonImage = (targetAge: number, images: MoonImage[]): MoonImage => {
return images.reduce((closest, current) => {
const currentDiff = Math.abs(current.age - targetAge);
const closestDiff = Math.abs(closest.age - targetAge);
return currentDiff < closestDiff ? current : closest;
});
};
```
#### 위치 정보 처리
- 위치 정보 획득 우선순위:
```typescript
const locationPriority = [
'userInput', // 사용자가 직접 입력한 위치
'browserGeo', // 브라우저 Geolocation API
'ipBased', // IP 기반 위치
'defaultSeoul' // 기본값 (서울)
];
```
- 위치 좌표 변환:
```typescript
interface GeocodingResult {
latitude: number; // 위도
longitude: number; // 경도
address: string; // 주소
formattedAddress: string; // 포맷된 주소
success: boolean; // 변환 성공 여부
error?: string; // 에러 메시지
}
// 위도/경도를 한국천문연구원 API 형식으로 변환
const formatCoordinates = (lat: number, lng: number) => {
const formatToDegreeMinute = (coordinate: number) => {
const degree = Math.floor(coordinate);
const minute = Math.round((coordinate - degree) * 60);
return `${degree}${minute.toString().padStart(2, '0')}`;
};
return {
latitude: formatToDegreeMinute(lat),
longitude: formatToDegreeMinute(lng),
dnYn: 'N' // 도분 형식 사용
};
};
```
### 성능 최적화 전략
```typescript
const performanceConfig = {
imagePreload: {
enabled: true,
threshold: 5 // 현재 날짜 기준 전후 5일
},
dataPreload: {
enabled: true,
monthsAhead: 1, // 다음 달 데이터 미리 로드
monthsBehind: 1 // 이전 달 데이터 미리 로드
}
};
```
### 캐싱 전략
```typescript
interface CacheConfig {
moonPhaseData: {
ttl: 3600000; // 1시간
maxEntries: 1000;
};
locationData: {
ttl: 86400000; // 24시간
maxEntries: 100;
};
}
```
### 2.2. 월간 달력 (Monthly Calendar)
#### 필수 기능
- 월별 달 위상 캘린더 뷰
- 주요 달 위상 하이라이트 (삭, 망, 상현, 하현)
- 날짜 선택시 상세 정보 모달
- 이전/다음 월 네비게이션
#### 구현 세부사항
- 캘린더 그리드 레이아웃
- 달 위상 데이터 캐싱 구현
- 반응형 디자인 지원
### 2.4. 애니메이션 (Animation)
#### 필수 기능
- 달 위상 변화 애니메이션
- 시간대별 달 위치 변화
- 인터랙티브 타임라인 슬라이더
#### 구현 세부사항
- 부드러운 애니메이션 처리
- 모바일 터치 제스처 지원
- 성능 최적화
## 5. 성능 요구사항
- 페이지 초기 로딩: 2초 이내
- API 응답 시간: 500ms 이내
- 애니메이션 프레임: 60fps 유지
- Lighthouse 성능 점수: 90점 이상
## 6. 보안 요구사항
- HTTPS 프로토콜 사용
- 위치 데이터 암호화
- XSS 및 CSRF 방어
## 7. 테스트 계획
### 7.1. 단위 테스트
- 천문 계산 함수 테스트
- API 엔드포인트 테스트
- UI 컴포넌트 테스트
### 7.2. 통합 테스트
- 프론트엔드-백엔드 통합 테스트
- 써드파티 API 연동 테스트
### 7.3. E2E 테스트
- 주요 사용자 시나리오 테스트
- 크로스 브라우저 테스트
- 모바일 디바이스 테스트
## 8. 배포 전략
- Docker 컨테이너화
- AWS ECS 활용
- CI/CD 파이프라인 구축
- Blue-Green 배포 방식 적용
## 9. 기술 상세 명세
### 9.1. 달령 계산 로직
```typescript
interface MoonPhaseCalculation {
// Julian Date 변환
getJulianDate: (date: Date) => number;
// 달령 계산
getMoonAge: (julianDate: number) => number;
// 달의 위상 계산
getMoonPhase: (age: number) => {
phase: string;
illumination: number;
koreanName: string;
englishName: string;
};
// 달의 밝기 계산
getMoonIllumination: (age: number) => number;
}
// 달령 계산 구현
const calculateMoonAge = (date: Date): number => {
const LUNAR_MONTH = 29.530588853; // 삭망월
const LUNAR_YEAR = 365.25; // 태양년
const julianDate = getJulianDate(date);
const newMoonEpoch = 2451550.1; // 2000년 1월 6일 18:14 UTC (새달)
const age = (julianDate - newMoonEpoch) % LUNAR_MONTH;
return age < 0 ? age + LUNAR_MONTH : age;
};
```
### 9.2. 데이터 구조 정의
```typescript
// 달 위상 데이터 구조
interface MoonPhaseData {
date: string;
age: number;
phase: {
name: {
ko: string;
en: string;
};
illumination: number;
category: MoonPhaseCategory;
};
times: {
rise: string;
set: string;
transit: string;
civilDawn: string; // 시민박명(아침)
civilDusk: string; // 시민박명(저녁)
nauticalDawn: string; // 항해박명(아침)
nauticalDusk: string; // 항해박명(저녁)
};
location: {
latitude: number;
longitude: number;
name?: string;
altitude?: number; // 고도
};
metadata: {
calculatedAt: string;
source: 'API' | 'CACHE' | 'CALCULATION';
accuracy: number; // 계산 정확도
};
}
// 상태 관리 인터페이스
interface MoonCalendarState {
currentPhase: MoonPhaseData;
selectedDate: Date;
monthlyData: Map<string, MoonPhaseData>;
location: LocationData;
settings: UserSettings;
uiState: UIState;
}
```
### 9.3. API 호출 및 에러 처리 전략
```typescript
// API 요청 관리자
class APIManager {
private static instance: APIManager;
private requestCount: number = 0;
private readonly DAILY_LIMIT = 10000;
// 요청 카운터 및 제한 관리
private async checkQuota(): Promise<boolean> {
const today = new Date().toISOString().split('T')[0];
const currentCount = await this.getRequestCount(today);
return currentCount < this.DAILY_LIMIT;
}
// 재시도 로직
private async retryStrategy<T>(
fn: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
try {
return await fn();
} catch (error) {
if (retries === 0) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
return this.retryStrategy(fn, retries - 1, delay * 2);
}
}
// 에러 처리
private handleError(error: any): never {
const errorMap = {
QUOTA_EXCEEDED: '일일 API 호출 한도 초과',
INVALID_LOCATION: '잘못된 위치 정보',
NETWORK_ERROR: '네트워크 오류',
INVALID_RESPONSE: '잘못된 응답 데이터'
};
throw new APIError(errorMap[error.code] || '알 수 없는 오류', error);
}
}
```
### 9.4. 캐싱 및 성능 최적화 전략
```typescript
// 캐시 관리자
class CacheManager {
private cache: Map<string, CacheEntry>;
// 계층형 캐싱 전략
private readonly CACHE_CONFIG = {
memory: {
ttl: 3600000, // 1시간
maxSize: 1000 // 항목 수
},
localStorage: {
ttl: 86400000, // 24시간
maxSize: 5000 // 항목 수
},
indexedDB: {
ttl: 604800000, // 1주일
maxSize: 10000 // 항목 수
}
};
// 캐시 정책
private getCachePolicy(data: MoonPhaseData): CachePolicy {
return {
priority: this.calculatePriority(data),
compression: this.shouldCompress(data),
persistence: this.determinePersistence(data)
};
}
// 캐시 무효화 전략
private invalidateCache(key: string): void {
const entry = this.cache.get(key);
if (entry && this.isStale(entry)) {
this.cache.delete(key);
this.notifyInvalidation(key);
}
}
}
```
### 9.5. 상태 관리 전략
```typescript
// 상태 관리 시스템
class StateManager {
private state: MoonCalendarState;
private subscribers: Set<StateSubscriber>;
// 상태 업데이트
public updateState(
updater: (state: MoonCalendarState) => Partial<MoonCalendarState>
): void {
const update = updater(this.state);
this.state = { ...this.state, ...update };
this.notifySubscribers();
}
// 실시간 업데이트 처리
private handleRealtimeUpdate(update: Partial<MoonCalendarState>): void {
this.updateState(() => update);
this.syncWithServer(update);
}
// 상태 지속성 관리
private persistState(): void {
localStorage.setItem('moonCalendarState', JSON.stringify(this.state));
}
}
```
### 9.6. 이미지 처리 최적화
```typescript
// 이미지 로더
class MoonImageLoader {
private imageCache: Map<string, HTMLImageElement>;
// 이미지 프리로딩
public preloadImages(phases: MoonPhaseData[]): void {
phases.forEach(phase => {
const img = new Image();
img.src = this.getImagePath(phase);
this.imageCache.set(phase.date, img);
});
}
// 이미지 최적화
private optimizeImage(
image: HTMLImageElement,
options: ImageOptimizationOptions
): Promise<Blob> {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 이미지 크기 최적화 및 압축
canvas.toBlob(resolve, 'image/webp', 0.8);
});
}
}
```
### 9.0. 백엔드 구현 전략
```typescript
// 백엔드 API 구조
interface MoonPhaseAPI {
// 달의 위상 계산 엔드포인트
'/api/v1/moon/phase': {
GET: {
params: {
date: string; // YYYY-MM-DD
latitude: number;
longitude: number;
};
response: MoonPhaseData;
};
};
// 월간 달 위상 데이터 엔드포인트
'/api/v1/moon/monthly': {
GET: {
params: {
year: number;
month: number;
latitude: number;
longitude: number;
};
response: Record<string, MoonPhaseData>;
};
};
}
// 하이브리드 모드 구현
interface HybridModeConfig {
// 오프라인 모드 감지
offlineDetection: {
checkInterval: 30000; // 30초
timeoutThreshold: 5000;// 5초
};
// 데이터 동기화 정책
syncPolicy: {
mode: 'ONLINE' | 'OFFLINE' | 'HYBRID';
priority: 'ACCURACY' | 'AVAILABILITY';
retryAttempts: 3;
retryDelay: 1000; // 1초
};
// 캐시 정책
cachePolicy: {
ttl: 3600000; // 1시간
maxEntries: 1000;
};
}
``` 천문연구원의 API를 활용해서 월출, 남중, 월몰의 데이터를 파싱해서 가지고 오세요.
SEO 최적화를 위한 전략을 수립하고 단계별로 진행하세요.
결과와 배운 점
이미 기능이 작성되어 잘 동작하는 기능도, 신규 기능을 추가하게 되면 코드를 수정하면서 이전 내용을 수정하거나 삭제하는 경우가 있어 간단한 웹사이트 이지만 기능을 수정하는데 많은 시간이 들어갔습니다.
기능 정의서를 만들고 해당 기능 정의서를 더욱 구체화 시키는 작업이 필요했으며, 각 컨포넌트들이 독립적인 기능을 수행하기 위한 프롬프트를 추가로 달아줘야 했습니다.
또한 신규 기능 추가 혹은 버그를 수정하는데 있어서 다른 기능들을 절대 수정하면 안된다는 규칙을 추가로 지정해줘서 프로젝트가 꼬일 수 있는 상황을 최소한으로 줄일 수 있었습니다.
도움 받은 글 (옵션)
참고한 지피터스 글이나 외부 사례를 알려주세요.
(내용 입력)