์ด๋ฒˆ ์ฃผ์— ์•Œ๊ฒŒ ๋œ ๊ฒƒ๋“ค

1. Presigned URL์˜ ๋งŒ๋ฃŒ ๋ฉ”์ปค๋‹ˆ์ฆ˜

์ตœ๊ทผ ์ด๋ฏธ์ง€ ํ”„๋ฆฌ๋กœ๋”ฉ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ 403 Forbidden ์—๋Ÿฌ๋ฅผ ๋งŒ๋‚ฌ๋‹ค. S3์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ Presigned URL๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋ฏธ๋ฆฌ ํ”„๋ฆฌ๋กœ๋“œํ•ด ๋‘์—ˆ๋Š”๋ฐ, ํ™”๋ฉด์—์„œ ์‹ค์ œ๋กœ ํ‘œ์‹œํ•˜๋ ค๋Š” ์‹œ์ ์— URL์ด ๋งŒ๋ฃŒ๋˜์–ด ์žˆ์—ˆ๋˜ ๊ฒƒ์ด๋‹ค. Presigned URL์€ ๋ณดํ†ต ๋‹ค์Œ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹ ์ค‘ ํ•˜๋‚˜๋กœ ๋งŒ๋ฃŒ ์‹œ๊ฐ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์—ˆ๊ณ , ์ด ์ •๋ณด๋ฅผ ์ด์šฉํ•ด URL์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ฐ„๋‹จํ•œ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

  • Azure: se ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋งŒ๋ฃŒ ์‹œ๊ฐ์ด ์ง์ ‘ ๋ช…์‹œ
  • AWS / GCP: ๋ฐœ๊ธ‰ ์‹œ๊ฐ๊ณผ ์œ ํšจ๊ธฐ๊ฐ„์˜ ํ•ฉ์œผ๋กœ ๊ณ„์‚ฐ
function getExpiryFromPresignedUrl(urlStr: string): number | undefined {
  const url = new URL(urlStr);

  // Azure: ๋งŒ๋ฃŒ ์‹œ๊ฐ์ด ์ง์ ‘ ๋ช…์‹œ๋จ
  const se = url.searchParams.get('se');
  if (se) {
    return new Date(se).getTime();
  }

  // AWS/GCP: ๋ฐœ๊ธ‰ ์‹œ๊ฐ + ์œ ํšจ๊ธฐ๊ฐ„์œผ๋กœ ๊ณ„์‚ฐ
  const date = url.searchParams.get('X-Amz-Date') || url.searchParams.get('X-Goog-Date');
  const expires = url.searchParams.get('X-Amz-Expires') || url.searchParams.get('X-Goog-Expires');

  if (date && expires) {
    const issuedAt = parseAmzDate(date);
    return issuedAt + Number(expires) * 1000;
  }

  return undefined;
}

function parseAmzDate(dateStr: string): number {
  // Format: 20251025T143000Z
  const year = dateStr.slice(0, 4);
  const month = dateStr.slice(4, 6);
  const day = dateStr.slice(6, 8);
  const hour = dateStr.slice(9, 11);
  const minute = dateStr.slice(11, 13);
  const second = dateStr.slice(13, 15);

  return Date.UTC(
    parseInt(year), 
    parseInt(month) - 1,
    parseInt(day),
    parseInt(hour),
    parseInt(minute),
    parseInt(second)
  );
}

์ด๋ ‡๊ฒŒ ๊ณ„์‚ฐ๋œ ๋งŒ๋ฃŒ ์‹œ๊ฐ์„ ๊ธฐ๋ฐ˜์œผ๋กœ, ๋งŒ๋ฃŒ๊ฐ€ ์ž„๋ฐ•ํ•œ URL์€ ๋ฏธ๋ฆฌ ์žฌ์š”์ฒญํ•˜๋„๋ก ํ•ด์„œ 403 ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

2. @starting-style

@starting-style์€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹œ์ž‘๋  ๋•Œ ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•˜๋Š” ๊ทœ์น™์ด๋‹ค. ๊ธฐ์กด์—๋Š” ํŠธ๋žœ์ง€์…˜์ด ์‹œ์ž‘๋˜๊ธฐ ์ „ ์ƒํƒœ๋ฅผ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ถ”๋ก ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ดˆ๊ธฐ ๋ Œ๋” ์‹œ ๊นœ๋นก์ž„์ด ์ƒ๊ธฐ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์—ˆ๋Š”๋ฐ, ํ•ด๋‹น ๋ถ€๋ถ„์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

.dialog {
  transition: opacity 0.3s;
  opacity: 1;
}

@starting-style {
  .dialog {
    opacity: 0;
  }
}