달의 모양을 확인하는 홈페이지

소개

기존의 검색엔진에 존재하는 달 모양 사이트의 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 최적화를 위한 전략을 수립하고 단계별로 진행하세요.

결과와 배운 점

이미 기능이 작성되어 잘 동작하는 기능도, 신규 기능을 추가하게 되면 코드를 수정하면서 이전 내용을 수정하거나 삭제하는 경우가 있어 간단한 웹사이트 이지만 기능을 수정하는데 많은 시간이 들어갔습니다.

기능 정의서를 만들고 해당 기능 정의서를 더욱 구체화 시키는 작업이 필요했으며, 각 컨포넌트들이 독립적인 기능을 수행하기 위한 프롬프트를 추가로 달아줘야 했습니다.

또한 신규 기능 추가 혹은 버그를 수정하는데 있어서 다른 기능들을 절대 수정하면 안된다는 규칙을 추가로 지정해줘서 프로젝트가 꼬일 수 있는 상황을 최소한으로 줄일 수 있었습니다.

도움 받은 글 (옵션)

참고한 지피터스 글이나 외부 사례를 알려주세요.

(내용 입력)

4
4개의 답글

👉 이 게시글도 읽어보세요