발행본 HTML 에 그 순간 기온·관측 시각이 박혀 구글과 첫 화면이 며칠 전 날씨를 봤다. 라운드·코랄·미니멀·솔숲은 관측 시각(시간대 없는 KST)을 new Date 로 읽어 굽는 서버(UTC)가 9시간 틀린 값을 박았고, 브라우저 값과 달라 열 때마다 React #418 → 페이지 전체를 다시 그렸다 (실측 /s/joyyy: HTML "22:45 관측" ↔ 브라우저 "13:45 관측"). - lib/use-live-weather.ts: 첫 렌더는 날씨 없음, 하이드레이션 뒤 스냅샷 → 최신 값 - layouts/{round,coral,minimal,pine}/Weather.tsx: 관측 시각을 글자 그대로 읽는다(다른 템플릿과 같게) - lib/format.ts formatKoreanDate: 한국 시간 기준 날짜 — UTC 서버가 0~9시 KST 발행에서 하루 전 날짜를 박던 것 - WeatherSection: 문구 고르는 부분을 weatherLineSources 로 떼어 테스트. HTML 에 날씨가 없음을 테스트로 고정 tsc·eslint 통과 · vitest 128 passed · TZ=UTC 로 구운 10개 사이트 #418 0건 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
/** "280000" → "280,000원". */
|
|
export function formatWon(value: string | number | null | undefined): string {
|
|
if (value == null || value === '') return '';
|
|
const num = typeof value === 'string' ? Number(value.replace(/,/g, '')) : value;
|
|
if (!Number.isFinite(num)) return String(value);
|
|
return `${num.toLocaleString('ko-KR')}원`;
|
|
}
|
|
|
|
/** ISO → "2026년 8월 27일". */
|
|
export function formatKoreanDate(iso: string | null | undefined): string {
|
|
if (!iso) return '';
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) return '';
|
|
const [year, month, day] = date.toLocaleDateString('sv-SE', {timeZone: 'Asia/Seoul'}).split('-').map(Number);
|
|
return `${year}년 ${month}월 ${day}일`;
|
|
}
|
|
|
|
/** 왜 한글 표기가 아닌가 답변 엔진은 본문에서 YYYY-MM-DD 를 찾아 최신성을 매긴다. */
|
|
export function isoDate(iso: string | null | undefined): string {
|
|
if (!iso) return '';
|
|
const date = new Date(iso);
|
|
if (Number.isNaN(date.getTime())) return '';
|
|
return date.toLocaleDateString('sv-SE', {timeZone: 'Asia/Seoul'});
|
|
}
|
|
|
|
/** 네이버 검색으로 보내는 링크. */
|
|
export function naverSearchUrl(query: string): string {
|
|
return `https://search.naver.com/search.naver?query=${encodeURIComponent(query)}`;
|
|
}
|
|
|
|
/** 네이버지도 검색으로 보내는 링크. */
|
|
export function naverMapSearchUrl(query: string): string {
|
|
return `https://map.naver.com/p/search/${encodeURIComponent(query)}`;
|
|
}
|
|
|
|
/** 유튜브 **검색** 주소. */
|
|
export function youtubeSearchUrl(query: string): string {
|
|
return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`;
|
|
}
|
|
|
|
/** 지도 주소에 실을 이름. */
|
|
function mapLabel(name: string): string {
|
|
return name.replace(/,/g, ' ').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
/** 카카오맵 **길찾기**. */
|
|
export function kakaoDirectionsUrl(name: string, lat?: number, lng?: number): string {
|
|
if (lat == null || lng == null) {
|
|
return `https://map.kakao.com/link/search/${encodeURIComponent(name)}`;
|
|
}
|
|
return `https://map.kakao.com/link/to/${encodeURIComponent(mapLabel(name))},${lat},${lng}`;
|
|
}
|
|
|
|
/** 네이버지도 **길찾기**. */
|
|
/** 네이버지도 길찾기 — **구간**. */
|
|
export function naverRouteUrl(
|
|
from: {name: string; lat: number; lng: number},
|
|
to: {name: string; lat: number; lng: number},
|
|
): string {
|
|
const point = (p: {name: string; lat: number; lng: number}) =>
|
|
`${p.lng},${p.lat},${encodeURIComponent(mapLabel(p.name))}`;
|
|
// 수단은 차(car)다.
|
|
return `https://map.naver.com/p/directions/${point(from)}/${point(to)}/-/car`;
|
|
}
|
|
|
|
export function naverDirectionsUrl(name: string, lat?: number, lng?: number): string {
|
|
if (lat == null || lng == null) {
|
|
return `https://map.naver.com/p/search/${encodeURIComponent(name)}`;
|
|
}
|
|
return `https://map.naver.com/p/directions/-/${lng},${lat},${encodeURIComponent(mapLabel(name))}/-/transit`;
|
|
}
|
|
|
|
/** 지도 그림(OpenStreetMap 임베드) 주소. */
|
|
export function osmEmbedUrl(lat: number, lng: number, aspect = 16 / 9, halfLat = 0.0022): string {
|
|
// bbox 의 종횡비를 iframe 과 맞춰야 한다.
|
|
const cos = Math.max(Math.cos((lat * Math.PI) / 180), 0.01);
|
|
const halfLng = (aspect * halfLat) / cos;
|
|
const bbox = [lng - halfLng, lat - halfLat, lng + halfLng, lat + halfLat]
|
|
.map((n) => n.toFixed(6))
|
|
.join(',');
|
|
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${lat},${lng}`;
|
|
}
|
|
|
|
/** 티맵 길찾기. */
|
|
export function tmapDirectionsUrl(name: string, lat?: number, lng?: number): string | undefined {
|
|
if (lat == null || lng == null) return undefined;
|
|
const q = encodeURIComponent(name);
|
|
return `tmap://route?goalname=${q}&goalx=${lng}&goaly=${lat}&rGoName=${q}&rGoX=${lng}&rGoY=${lat}`;
|
|
}
|