diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e1a127..da51daf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,4 +26,6 @@ jobs: - run: npm ci - run: npm run lint - run: npm run type-check + - run: npm run test:post-dates - run: npm run build + - run: npm run test:sitemap-lastmod diff --git a/README.md b/README.md index fcc0487..fca8008 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ npm run dev --- title: "제목" date: "2025-01-01" +updated: "2025-02-01" # 최종 수정일 description: "설명" tags: ["react", "typescript"] series: "시리즈명" # optional @@ -135,6 +136,7 @@ publishDate: "2025-12-01" # optional, 예약 발행 ``` `draft: true` 또는 미래의 `publishDate`가 있는 글은 production 빌드에서 자동으로 제외됩니다. +`date`는 최초 게시일, `updated`는 최종 수정일이며 둘 다 필수입니다. 처음 게시할 때는 같은 값을 기록하고, 이후 본문·구조화 데이터·주요 링크처럼 검색 결과에 영향을 주는 내용을 실제로 수정했을 때만 `updated`를 변경합니다. 게시일보다 빠르거나 미래인 수정일은 빌드에서 거부됩니다. ## 프로젝트 구조 diff --git a/content/posts/en/api-response-error-standardization.md b/content/posts/en/api-response-error-standardization.md index 6e51c7c..b804397 100644 --- a/content/posts/en/api-response-error-standardization.md +++ b/content/posts/en/api-response-error-standardization.md @@ -1,6 +1,7 @@ --- title: "Standardizing API Responses and Error Handling - Getting HTTP Status Codes Right" date: "2026-03-10" +updated: "2026-03-10" description: "How I migrated from a legacy structure where every API returned 200 to a shared response and error system that uses HTTP status codes properly, improving monitoring, debugging, and team communication." tags: ["java", "spring-boot", "architecture", "error-handling", "refactoring"] draft: false diff --git a/content/posts/en/auth-authorize-callback-flow.md b/content/posts/en/auth-authorize-callback-flow.md index 2e6c15f..96bbb7e 100644 --- a/content/posts/en/auth-authorize-callback-flow.md +++ b/content/posts/en/auth-authorize-callback-flow.md @@ -1,6 +1,7 @@ --- title: "Splitting Login Responsibility (2) - Connecting the authorize/callback Flow" date: "2026-04-16" +updated: "2026-04-16" description: "How the frontend connects the login flow from protected page access to backend authorize, callback, and token issuance." tags: ["authentication", "oauth", "frontend", "backend", "typescript", "java", "spring"] series: "Splitting Login Responsibility" diff --git a/content/posts/en/auth-token-verification-migration.md b/content/posts/en/auth-token-verification-migration.md index bc078a8..7ed9772 100644 --- a/content/posts/en/auth-token-verification-migration.md +++ b/content/posts/en/auth-token-verification-migration.md @@ -1,6 +1,7 @@ --- title: "Splitting Login Responsibility (3) - Verifying Old Tokens and New Backend Tokens Together" date: "2026-04-17" +updated: "2026-04-17" description: "How I split verification order and failure handling when old frontend tokens and new backend-issued tokens had to be accepted through the same browser cookies." tags: ["authentication", "oauth", "jwt", "testing", "migration", "java", "spring"] series: "Splitting Login Responsibility" diff --git a/content/posts/en/backend-owned-login-flow.md b/content/posts/en/backend-owned-login-flow.md index 6f70502..588c77d 100644 --- a/content/posts/en/backend-owned-login-flow.md +++ b/content/posts/en/backend-owned-login-flow.md @@ -1,6 +1,7 @@ --- title: "Splitting Login Responsibility (1) - Why I Moved It from Frontend to Backend" date: "2026-04-15" +updated: "2026-04-15" description: "The responsibility boundaries I used while moving login state from a frontend-owned flow to a backend-owned authentication flow." tags: ["authentication", "oauth", "backend", "frontend", "architecture"] series: "Splitting Login Responsibility" diff --git a/content/posts/en/blog-dev-01-intro.md b/content/posts/en/blog-dev-01-intro.md index e61046f..0bf75f2 100644 --- a/content/posts/en/blog-dev-01-intro.md +++ b/content/posts/en/blog-dev-01-intro.md @@ -1,6 +1,7 @@ --- title: "Building a Blog #1 - Project Overview and Tech Stack" date: "2026-02-26" +updated: "2026-02-26" description: "Why I rebuilt my personal blog with React 19, Vite 7, and shadcn/ui, and how the project is structured." tags: ["react", "vite", "shadcn-ui", "blog"] series: "Building a React Blog" diff --git a/content/posts/en/blog-dev-02-search-transitions.md b/content/posts/en/blog-dev-02-search-transitions.md index c2b1dc4..f44a268 100644 --- a/content/posts/en/blog-dev-02-search-transitions.md +++ b/content/posts/en/blog-dev-02-search-transitions.md @@ -1,6 +1,7 @@ --- title: "Building a Blog #2 - Command Palette Search and View Transitions" date: "2026-02-26" +updated: "2026-02-26" description: "How I implemented Cmd+K search with cmdk and added page transition animation with the View Transitions API." tags: ["react", "cmdk", "view-transitions", "blog"] series: "Building a React Blog" diff --git a/content/posts/en/blog-dev-03-theme-series.md b/content/posts/en/blog-dev-03-theme-series.md index 384f9ec..f2e10b2 100644 --- a/content/posts/en/blog-dev-03-theme-series.md +++ b/content/posts/en/blog-dev-03-theme-series.md @@ -1,6 +1,7 @@ --- title: "Building a Blog #3 - Color Theme Presets and Post Series" date: "2026-02-26" +updated: "2026-02-26" description: "How I created six color presets with shadcn/ui's oklch color system and implemented post series navigation." tags: ["react", "shadcn-ui", "css", "blog"] series: "Building a React Blog" diff --git a/content/posts/en/blog-dev-04-search-seo.md b/content/posts/en/blog-dev-04-search-seo.md index b5d38fb..d5eb9e3 100644 --- a/content/posts/en/blog-dev-04-search-seo.md +++ b/content/posts/en/blog-dev-04-search-seo.md @@ -1,6 +1,7 @@ --- title: "Building a Blog #4 - Advanced Search and SEO Optimization" date: "2026-02-26" +updated: "2026-02-26" description: "How I built a search page with date and tag filters, then improved SEO with meta tags, sitemap, and RSS feed generation." tags: ["react", "seo", "shadcn-ui", "blog"] series: "Building a React Blog" diff --git a/content/posts/en/blog-dev-05-analytics.md b/content/posts/en/blog-dev-05-analytics.md index 2280cec..0b1258c 100644 --- a/content/posts/en/blog-dev-05-analytics.md +++ b/content/posts/en/blog-dev-05-analytics.md @@ -1,6 +1,7 @@ --- title: "Building a Blog #5 - Google Analytics Integration and View Counts" date: "2026-02-26" +updated: "2026-02-26" description: "How I display visitor counts and daily page view charts on a static site using GA4 data." tags: ["react", "google-analytics", "shadcn-ui", "blog"] series: "Building a React Blog" diff --git a/content/posts/en/blog-dev-06-ux-seo.md b/content/posts/en/blog-dev-06-ux-seo.md index 7542173..36d1728 100644 --- a/content/posts/en/blog-dev-06-ux-seo.md +++ b/content/posts/en/blog-dev-06-ux-seo.md @@ -1,6 +1,7 @@ --- title: "Building a Blog #6 - UX Improvements and Final SEO Polish" date: "2026-02-26" +updated: "2026-02-26" description: "Year-based post grouping, mobile sidebar UX improvements, robots.txt, OG image setup, and other small refinements that make the blog feel more complete." tags: ["react", "seo", "ux", "blog"] series: "Building a React Blog" diff --git a/content/posts/en/ecs-to-eks-migration.md b/content/posts/en/ecs-to-eks-migration.md index 6914c65..421f2ed 100644 --- a/content/posts/en/ecs-to-eks-migration.md +++ b/content/posts/en/ecs-to-eks-migration.md @@ -1,6 +1,7 @@ --- title: "From ECS to EKS - Building an Operating Environment with kustomize and Gateway API" date: "2026-03-12" +updated: "2026-03-12" description: "What I learned while migrating from AWS ECS to EKS and building an operating model with kustomize base/overlay, Kubernetes Gateway API, Karpenter node autoscaling, and blue/green deployment." tags: ["kubernetes", "eks", "kustomize", "gateway-api", "karpenter", "devops", "aws"] series: "Service Infrastructure Modernization" diff --git a/content/posts/en/gitops-argocd-n8n-deployment.md b/content/posts/en/gitops-argocd-n8n-deployment.md index 89c2ef7..6bce7bd 100644 --- a/content/posts/en/gitops-argocd-n8n-deployment.md +++ b/content/posts/en/gitops-argocd-n8n-deployment.md @@ -1,6 +1,7 @@ --- title: "GitOps Deployment Automation - Building a Slack Approval Pipeline with ArgoCD, n8n, and GitHub Actions" date: "2026-02-26" +updated: "2026-02-26" description: "How I built a GitOps-based deployment automation system during the ECS to EKS migration, combining ArgoCD, n8n, and GitHub Actions with different deployment strategies per environment." tags: ["kubernetes", "argocd", "gitops", "n8n", "devops", "github-actions"] draft: false diff --git a/content/posts/en/hello-world.md b/content/posts/en/hello-world.md index 7cbbdf8..ff42fd3 100644 --- a/content/posts/en/hello-world.md +++ b/content/posts/en/hello-world.md @@ -1,6 +1,7 @@ --- title: "Hello World" date: "2026-02-25" +updated: "2026-02-25" description: "The first post on this blog." tags: ["blog"] draft: true diff --git a/content/posts/en/i18n-01-foundation.md b/content/posts/en/i18n-01-foundation.md index ebcc014..5650ced 100644 --- a/content/posts/en/i18n-01-foundation.md +++ b/content/posts/en/i18n-01-foundation.md @@ -1,6 +1,7 @@ --- title: "Service Internationalization (1) - Stripping Out Hardcoded Korean" date: "2026-05-19" +updated: "2026-05-19" description: "Frontend internationalization, kicked off ahead of our launch in Japan. Here's how we settled on a locale model, managed our messages, and flowed the locale through the entire system." tags: ["i18n", "nextjs", "react", "typescript"] series: "Service Internationalization" diff --git a/content/posts/en/i18n-02-language-pack-gcs.md b/content/posts/en/i18n-02-language-pack-gcs.md index f1c21ea..9164f30 100644 --- a/content/posts/en/i18n-02-language-pack-gcs.md +++ b/content/posts/en/i18n-02-language-pack-gcs.md @@ -1,6 +1,7 @@ --- title: "Service Internationalization (2) - Serving Language Packs: Bundle, GCS Runtime Load, and Cache" date: "2026-06-03" +updated: "2026-06-03" description: "If you ship translations together with the app, every one-line copy fix means a redeploy. This post covers the structure that loads language packs from GCS at runtime and falls back to the bundle, plus the cache problem where copy wouldn't change even after deploying." tags: ["i18n", "nextjs", "gcs", "typescript"] series: "Service Internationalization" diff --git a/content/posts/en/i18n-03-rollout-killswitch.md b/content/posts/en/i18n-03-rollout-killswitch.md index 3a4869c..503720d 100644 --- a/content/posts/en/i18n-03-rollout-killswitch.md +++ b/content/posts/en/i18n-03-rollout-killswitch.md @@ -1,6 +1,7 @@ --- title: "Service Internationalization (3) - Zero-Downtime Rollout with a GrowthBook Kill Switch" date: "2026-06-17" +updated: "2026-06-17" description: "Even with all translations ready, flipping the switch for everyone at once is risky. The story of funneling locale decisions into a single choke point and rolling out i18n with zero downtime, gradually, via a GrowthBook kill switch." tags: ["i18n", "nextjs", "growthbook", "feature-flag"] series: "Service Internationalization" diff --git a/content/posts/en/i18n-04-backend-locale.md b/content/posts/en/i18n-04-backend-locale.md index cf485cd..02e74ba 100644 --- a/content/posts/en/i18n-04-backend-locale.md +++ b/content/posts/en/i18n-04-backend-locale.md @@ -1,6 +1,7 @@ --- title: "Service Internationalization (4) - The Backend's Part: User Language, Timezone, and Document Locale" date: "2026-06-26" +updated: "2026-06-26" description: "Translate the front end alone and the server keeps mixing Korean back in through its responses and documents. This is the story of how the backend stored each user's language and timezone, carried them along in the authentication context, and pushed the locale all the way through to responses and documents." tags: ["i18n", "spring-boot", "java", "backend"] series: "Service Internationalization" diff --git a/content/posts/en/i18n-05-document-render.md b/content/posts/en/i18n-05-document-render.md index a316c67..917684c 100644 --- a/content/posts/en/i18n-05-document-render.md +++ b/content/posts/en/i18n-05-document-render.md @@ -1,6 +1,7 @@ --- title: "Service Internationalization (5) - From Image-Baked Documents to HTML: Multilingual PDF Rendering" date: "2026-07-04" +updated: "2026-07-04" description: "For a long time, the language of our certificates and reports was baked into images. This is the story of moving image-based documents to HTML templates plus Cloud Run rendering, so that adding a new language became a single JSON file." tags: ["i18n", "cloud-run", "pdf", "weasyprint"] series: "Service Internationalization" diff --git a/content/posts/en/jdk-migration-strategy-11-to-25.md b/content/posts/en/jdk-migration-strategy-11-to-25.md index 2813900..f8f0b85 100644 --- a/content/posts/en/jdk-migration-strategy-11-to-25.md +++ b/content/posts/en/jdk-migration-strategy-11-to-25.md @@ -1,6 +1,7 @@ --- title: "JDK Migration (1) - From JDK 11 to 21 and the Spring Boot 3.x Transition" date: "2026-03-18" +updated: "2026-03-18" description: "A practical record of upgrading a production Spring Boot service from JDK 11 to 21, including the javax to jakarta transition, AWS SDK migration, and QueryDSL compatibility work." tags: ["java", "jdk", "spring-boot", "migration", "devops"] series: "JDK Migration" diff --git a/content/posts/en/jdk-migration-strategy-21-to-25.md b/content/posts/en/jdk-migration-strategy-21-to-25.md index dc14df0..f4bc947 100644 --- a/content/posts/en/jdk-migration-strategy-21-to-25.md +++ b/content/posts/en/jdk-migration-strategy-21-to-25.md @@ -1,6 +1,7 @@ --- title: "JDK Migration (2) - From JDK 21 to 25, Virtual Threads, and Modern Java" date: "2026-03-18" +updated: "2026-03-18" description: "A practical record of upgrading from JDK 21 to 25, enabling Virtual Threads, handling pinning issues, applying pattern matching switch, and moving to Gradle Kotlin DSL." tags: ["java", "jdk", "spring-boot", "virtual-threads", "devops"] series: "JDK Migration" diff --git a/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md b/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md index 626af45..afc7cda 100644 --- a/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md +++ b/content/posts/en/jwt-hs256-to-rs256-jwks-kms.md @@ -1,6 +1,7 @@ --- title: "JWT Signing Migration - From HS256 to RS256/JWKS/KMS" date: "2026-04-24" +updated: "2026-04-24" description: "How I split signing responsibility, kid, key rotation, and legacy token handling while moving symmetric JWT signing to RS256, JWKS, and KMS." tags: ["authentication", "jwt", "jwks", "kms", "java", "spring", "security"] draft: false diff --git a/content/posts/en/lgtm-stack-observability.md b/content/posts/en/lgtm-stack-observability.md index 61cb468..9d40bd0 100644 --- a/content/posts/en/lgtm-stack-observability.md +++ b/content/posts/en/lgtm-stack-observability.md @@ -1,6 +1,7 @@ --- title: "Replacing Datadog with an LGTM Stack - OTel Sidecar with Grafana, Loki, Tempo, and Mimir" date: "2026-03-17" +updated: "2026-03-17" description: "How I moved from Datadog to an open-source LGTM stack and built observability with an OTel Collector sidecar, noise filtering, MDC request tracing, and IRSA-backed S3 storage." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "observability", "devops"] series: "Service Infrastructure Modernization" diff --git a/content/posts/en/lgtm-stack-setup-guide.md b/content/posts/en/lgtm-stack-setup-guide.md index fe84796..3ee6032 100644 --- a/content/posts/en/lgtm-stack-setup-guide.md +++ b/content/posts/en/lgtm-stack-setup-guide.md @@ -1,6 +1,7 @@ --- title: "Building an LGTM Stack on EKS - Practical Setup Guide for Loki, Grafana, Tempo, and Mimir" date: "2026-03-17" +updated: "2026-03-17" description: "A practical guide to deploying Grafana's LGTM stack on EKS with Helm and collecting Spring Boot logs, traces, and metrics through an OTel Collector sidecar." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "helm", "observability"] draft: false diff --git a/content/posts/en/multi-channel-notification-server.md b/content/posts/en/multi-channel-notification-server.md index 436fe12..1a47c80 100644 --- a/content/posts/en/multi-channel-notification-server.md +++ b/content/posts/en/multi-channel-notification-server.md @@ -1,6 +1,7 @@ --- title: "Building a Multi-Channel Notification Server - From PHP Legacy to Java/Spring" date: "2026-03-09" +updated: "2026-03-09" description: "How I moved scattered PHP notification logic into an independent Spring Boot notification server that handles Kakao AlimTalk, SMS, email, push, and Slack through one structure." tags: ["java", "spring-boot", "notification", "sqs", "architecture"] draft: false diff --git a/content/posts/en/payment-system-migration-php-to-spring.md b/content/posts/en/payment-system-migration-php-to-spring.md index 39a76e1..1d95254 100644 --- a/content/posts/en/payment-system-migration-php-to-spring.md +++ b/content/posts/en/payment-system-migration-php-to-spring.md @@ -1,6 +1,7 @@ --- title: "Payment System Redesign (1) - From PHP Legacy to Java/Spring" date: "2026-03-06" +updated: "2026-03-06" description: "How I migrated a legacy payment system written in PHP to a Java/Spring and SQS-based event-driven architecture." tags: ["java", "spring-boot", "payment", "sqs", "migration"] series: "Payment System Redesign" diff --git a/content/posts/en/payment-system-redesign-sync-api.md b/content/posts/en/payment-system-redesign-sync-api.md index 1dee9b5..e6e7f26 100644 --- a/content/posts/en/payment-system-redesign-sync-api.md +++ b/content/posts/en/payment-system-redesign-sync-api.md @@ -1,6 +1,7 @@ --- title: "Payment System Redesign (2) - Removing SQS and Moving to Synchronous APIs" date: "2026-03-06" +updated: "2026-03-06" description: "SQS looked attractive as an event-driven architecture, but in the payment domain it added more complexity than value. This post covers how I removed SQS and stabilized payment processing with synchronous APIs and Portone webhooks." tags: ["java", "spring-boot", "payment", "portone", "refactoring"] series: "Payment System Redesign" diff --git a/content/posts/en/react-hooks-deep-dive.md b/content/posts/en/react-hooks-deep-dive.md index 27b559e..bdee1c7 100644 --- a/content/posts/en/react-hooks-deep-dive.md +++ b/content/posts/en/react-hooks-deep-dive.md @@ -1,6 +1,7 @@ --- title: "React Hooks Deep Dive" date: "2026-02-26" +updated: "2026-02-26" description: "A closer look at how React Hooks work and the practical patterns I use in real projects." tags: ["react", "hooks", "typescript"] series: "Mastering React" diff --git a/content/posts/en/react-typescript-tips.md b/content/posts/en/react-typescript-tips.md index ee79a8f..698b1ea 100644 --- a/content/posts/en/react-typescript-tips.md +++ b/content/posts/en/react-typescript-tips.md @@ -1,6 +1,7 @@ --- title: "Practical React + TypeScript Tips" date: "2026-02-20" +updated: "2026-02-20" description: "Practical TypeScript patterns that make React projects safer and easier to maintain." tags: ["react", "typescript"] series: "Mastering React" diff --git a/content/posts/en/spring-ai-cs-automation.md b/content/posts/en/spring-ai-cs-automation.md index 4d82582..f5fbebf 100644 --- a/content/posts/en/spring-ai-cs-automation.md +++ b/content/posts/en/spring-ai-cs-automation.md @@ -1,6 +1,7 @@ --- title: "Building a CS Automation System with Spring AI - Function Calling and FAQ Instructions" date: "2026-07-06" +updated: "2026-07-06" description: "How we automated repetitive customer inquiries with Spring AI - using function calling to fetch user context and registering FAQs as instructions to combine auto-replies with draft recommendations." tags: ["spring-ai", "spring-boot", "ai", "llm", "cs"] draft: false diff --git a/content/posts/en/spring-ai-guide-01-setup.md b/content/posts/en/spring-ai-guide-01-setup.md index 7123a6e..8b9207e 100644 --- a/content/posts/en/spring-ai-guide-01-setup.md +++ b/content/posts/en/spring-ai-guide-01-setup.md @@ -1,6 +1,7 @@ --- title: "Spring AI Guide (1) - From Project Setup to the First AI Call" date: "2026-03-17" +updated: "2026-03-17" description: "A guide to Spring AI core concepts, project setup, and the first AI call with ChatClient." tags: ["spring-ai", "spring-boot", "ai", "llm", "java"] series: "Spring AI Guide" diff --git a/content/posts/en/spring-ai-guide-02-multi-provider.md b/content/posts/en/spring-ai-guide-02-multi-provider.md index 86ab6b8..d0d2078 100644 --- a/content/posts/en/spring-ai-guide-02-multi-provider.md +++ b/content/posts/en/spring-ai-guide-02-multi-provider.md @@ -1,6 +1,7 @@ --- title: "Spring AI Guide (2) - Multi-Provider Strategy" date: "2026-03-17" +updated: "2026-03-17" description: "How to abstract OpenAI, AWS Bedrock, and Google Gemini behind one ChatClient interface and switch providers dynamically at runtime." tags: ["spring-ai", "spring-boot", "ai", "openai", "bedrock", "gemini"] series: "Spring AI Guide" diff --git a/content/posts/en/spring-ai-guide-03-prompt-structured-output.md b/content/posts/en/spring-ai-guide-03-prompt-structured-output.md index 0fa4569..2fd4f87 100644 --- a/content/posts/en/spring-ai-guide-03-prompt-structured-output.md +++ b/content/posts/en/spring-ai-guide-03-prompt-structured-output.md @@ -1,6 +1,7 @@ --- title: "Spring AI Guide (3) - Prompt Management and Structured Output" date: "2026-03-17" +updated: "2026-03-17" description: "How to use Spring AI's message model, variable templating, database-backed prompt management, and JSON Schema-based structured output." tags: ["spring-ai", "spring-boot", "ai", "prompt-engineering", "structured-output"] series: "Spring AI Guide" diff --git a/content/posts/en/spring-ai-pipeline-real-world.md b/content/posts/en/spring-ai-pipeline-real-world.md index 1339448..1bf56f7 100644 --- a/content/posts/en/spring-ai-pipeline-real-world.md +++ b/content/posts/en/spring-ai-pipeline-real-world.md @@ -1,6 +1,7 @@ --- title: "Spring AI in Practice - Building a Seven-Step AI Diagnostic Pipeline" date: "2026-02-28" +updated: "2026-02-28" description: "A real-world record of redesigning direct HTTP calls and one large synchronous prompt into a seven-step Spring AI pipeline." tags: ["spring-ai", "spring-boot", "ai", "llm"] draft: false diff --git a/content/posts/en/spring-facade-pattern-layered-architecture.md b/content/posts/en/spring-facade-pattern-layered-architecture.md index b83c79d..6d2f16b 100644 --- a/content/posts/en/spring-facade-pattern-layered-architecture.md +++ b/content/posts/en/spring-facade-pattern-layered-architecture.md @@ -1,6 +1,7 @@ --- title: "Introducing the Facade Pattern to Spring MVC - Improving Layered Architecture with a @Gateway Annotation" date: "2026-03-10" +updated: "2026-03-10" description: "How I introduced a custom @Gateway annotation and facade layer into a traditional Controller-Service-Repository architecture to clarify domain responsibilities and improve team productivity." tags: ["java", "spring-boot", "architecture", "design-pattern", "facade"] draft: false diff --git a/content/posts/en/sync-virtual-coroutine-benchmark.md b/content/posts/en/sync-virtual-coroutine-benchmark.md index 652976d..93fbef6 100644 --- a/content/posts/en/sync-virtual-coroutine-benchmark.md +++ b/content/posts/en/sync-virtual-coroutine-benchmark.md @@ -1,6 +1,7 @@ --- title: "Platform Thread vs Virtual Thread vs Coroutine - 10,000 Task Benchmark" date: "2026-03-24" +updated: "2026-03-24" description: "A benchmark comparing JDK 25 Platform Threads, Virtual Threads, and Kotlin Coroutines across I/O-bound, CPU-bound, and high-concurrency scenarios with 10,000 tasks repeated 100 times." tags: ["java", "kotlin", "virtual-threads", "coroutine", "benchmark", "concurrency"] draft: false diff --git a/content/posts/en/tailwind-css-guide.md b/content/posts/en/tailwind-css-guide.md index 6437293..8847a9c 100644 --- a/content/posts/en/tailwind-css-guide.md +++ b/content/posts/en/tailwind-css-guide.md @@ -1,6 +1,7 @@ --- title: "Getting Started with Tailwind CSS" date: "2026-02-15" +updated: "2026-02-15" description: "A quick look at the core ideas and common usage patterns of Tailwind CSS." tags: ["css", "tailwind"] draft: true diff --git a/content/posts/ko/api-response-error-standardization.md b/content/posts/ko/api-response-error-standardization.md index 181cd50..9ea077c 100644 --- a/content/posts/ko/api-response-error-standardization.md +++ b/content/posts/ko/api-response-error-standardization.md @@ -1,6 +1,7 @@ --- title: "API 응답/에러 처리 구조 공통화 - HTTP 상태코드를 제대로 쓰기까지" date: "2026-03-10" +updated: "2026-03-10" description: "모든 API가 200을 반환하던 레거시 구조에서, HTTP 상태코드를 올바르게 활용하는 공통 응답/에러 체계로 전면 개선한 과정. 모니터링 정상화, 디버깅 효율화, 개발자 간 커뮤니케이션 비용 절감까지." tags: ["java", "spring-boot", "architecture", "error-handling", "refactoring"] draft: false diff --git a/content/posts/ko/auth-authorize-callback-flow.md b/content/posts/ko/auth-authorize-callback-flow.md index e00e8a9..ce7dd36 100644 --- a/content/posts/ko/auth-authorize-callback-flow.md +++ b/content/posts/ko/auth-authorize-callback-flow.md @@ -1,6 +1,7 @@ --- title: "로그인 책임 분리 (2) - authorize/callback 흐름 연결하기" date: "2026-04-16" +updated: "2026-04-16" description: "보호 페이지 접근부터 백엔드 authorize, callback, 토큰 발급까지 이어지는 로그인 흐름을 프론트엔드에서 어떻게 연결했는지 정리합니다." tags: ["authentication", "oauth", "frontend", "backend", "typescript", "java", "spring"] series: "로그인 책임 분리" diff --git a/content/posts/ko/auth-token-verification-migration.md b/content/posts/ko/auth-token-verification-migration.md index e12923e..ea0c734 100644 --- a/content/posts/ko/auth-token-verification-migration.md +++ b/content/posts/ko/auth-token-verification-migration.md @@ -1,6 +1,7 @@ --- title: "로그인 책임 분리 (3) - 기존 토큰과 새 백엔드 토큰을 함께 검증하기" date: "2026-04-17" +updated: "2026-04-17" description: "브라우저에는 토큰이 쿠키로 전달되지만 기존 토큰과 새 백엔드 토큰의 검증 기준이 다를 때 순서와 실패 처리를 어떻게 나눴는지 정리합니다." tags: ["authentication", "oauth", "jwt", "testing", "migration", "java", "spring"] series: "로그인 책임 분리" diff --git a/content/posts/ko/aws-to-gcp-migration.md b/content/posts/ko/aws-to-gcp-migration.md index 1f0ed87..16bd1e0 100644 --- a/content/posts/ko/aws-to-gcp-migration.md +++ b/content/posts/ko/aws-to-gcp-migration.md @@ -1,6 +1,7 @@ --- title: "AWS에서 GCP로 - 클라우드 이관에서 실제로 바뀐 것들" date: "2026-06-18" +updated: "2026-06-18" description: "AWS에서 GCP로 서비스를 옮기면서 런타임, 배포, 시크릿, 데이터 연결, 캐시, 관측성의 기준이 어떻게 바뀌었는지 정리합니다." tags: ["gcp", "aws", "migration", "kubernetes", "devops", "cloud"] series: "서비스 인프라 현대화" diff --git a/content/posts/ko/backend-owned-login-flow.md b/content/posts/ko/backend-owned-login-flow.md index e10ce75..4da3f1c 100644 --- a/content/posts/ko/backend-owned-login-flow.md +++ b/content/posts/ko/backend-owned-login-flow.md @@ -1,6 +1,7 @@ --- title: "로그인 책임 분리 (1) - 프론트엔드에서 백엔드로 옮긴 이유" date: "2026-04-15" +updated: "2026-04-15" description: "프론트엔드가 로그인 상태를 직접 만들던 구조를 백엔드 중심의 인증 흐름으로 바꾸며 정리한 책임 분리 기준." tags: ["authentication", "oauth", "backend", "frontend", "architecture"] series: "로그인 책임 분리" diff --git a/content/posts/ko/blog-dev-01-intro.md b/content/posts/ko/blog-dev-01-intro.md index 13762ca..8582b94 100644 --- a/content/posts/ko/blog-dev-01-intro.md +++ b/content/posts/ko/blog-dev-01-intro.md @@ -1,6 +1,7 @@ --- title: "블로그 만들기 #1 - 프로젝트 소개 & 기술 스택" date: "2026-02-26" +updated: "2026-02-26" description: "React 19, Vite 7, shadcn/ui로 개인 블로그를 새로 만든 이유와 기술 스택을 소개합니다." tags: ["react", "vite", "shadcn-ui", "blog"] series: "React 블로그 만들기" diff --git a/content/posts/ko/blog-dev-02-search-transitions.md b/content/posts/ko/blog-dev-02-search-transitions.md index 0aee0ad..dcb869f 100644 --- a/content/posts/ko/blog-dev-02-search-transitions.md +++ b/content/posts/ko/blog-dev-02-search-transitions.md @@ -1,6 +1,7 @@ --- title: "블로그 만들기 #2 - Command Palette 검색 & View Transitions" date: "2026-02-26" +updated: "2026-02-26" description: "cmdk로 Cmd+K 검색을 구현하고 View Transitions API로 페이지 전환 애니메이션을 적용한 과정입니다." tags: ["react", "cmdk", "view-transitions", "blog"] series: "React 블로그 만들기" diff --git a/content/posts/ko/blog-dev-03-theme-series.md b/content/posts/ko/blog-dev-03-theme-series.md index 1bad1a9..5faa5cf 100644 --- a/content/posts/ko/blog-dev-03-theme-series.md +++ b/content/posts/ko/blog-dev-03-theme-series.md @@ -1,6 +1,7 @@ --- title: "블로그 만들기 #3 - 컬러 테마 프리셋 & 포스트 시리즈" date: "2026-02-26" +updated: "2026-02-26" description: "shadcn/ui의 oklch 색상 시스템으로 6가지 컬러 프리셋을 만들고 포스트 시리즈 기능을 구현한 과정입니다." tags: ["react", "shadcn-ui", "css", "blog"] series: "React 블로그 만들기" diff --git a/content/posts/ko/blog-dev-04-search-seo.md b/content/posts/ko/blog-dev-04-search-seo.md index b177f2f..27ca219 100644 --- a/content/posts/ko/blog-dev-04-search-seo.md +++ b/content/posts/ko/blog-dev-04-search-seo.md @@ -1,6 +1,7 @@ --- title: "블로그 만들기 #4 - 고급 검색 & SEO 최적화" date: "2026-02-26" +updated: "2026-02-26" description: "날짜/태그 필터가 포함된 검색 페이지를 만들고 메타 태그, sitemap, RSS 피드로 SEO를 최적화한 과정입니다." tags: ["react", "seo", "shadcn-ui", "blog"] series: "React 블로그 만들기" diff --git a/content/posts/ko/blog-dev-05-analytics.md b/content/posts/ko/blog-dev-05-analytics.md index e7587c7..b0df373 100644 --- a/content/posts/ko/blog-dev-05-analytics.md +++ b/content/posts/ko/blog-dev-05-analytics.md @@ -1,6 +1,7 @@ --- title: "블로그 만들기 #5 - Google Analytics 연동 & 방문자 수 표시" date: "2026-02-26" +updated: "2026-02-26" description: "정적 사이트에서 GA4 데이터를 활용해 방문자 수와 일별 조회수 차트를 표시하는 방법을 정리합니다." tags: ["react", "google-analytics", "shadcn-ui", "blog"] series: "React 블로그 만들기" diff --git a/content/posts/ko/blog-dev-06-ux-seo.md b/content/posts/ko/blog-dev-06-ux-seo.md index 40f4f7e..4d28efd 100644 --- a/content/posts/ko/blog-dev-06-ux-seo.md +++ b/content/posts/ko/blog-dev-06-ux-seo.md @@ -1,6 +1,7 @@ --- title: "블로그 만들기 #6 - UX 개선 & SEO 마무리" date: "2026-02-26" +updated: "2026-02-26" description: "글 목록 연도별 그룹핑, 모바일 사이드바 UX 개선, robots.txt, OG 이미지 등 블로그의 완성도를 높이는 작업들을 정리합니다." tags: ["react", "seo", "ux", "blog"] series: "React 블로그 만들기" diff --git a/content/posts/ko/ecs-to-eks-migration.md b/content/posts/ko/ecs-to-eks-migration.md index 1ad06eb..b4e9ff7 100644 --- a/content/posts/ko/ecs-to-eks-migration.md +++ b/content/posts/ko/ecs-to-eks-migration.md @@ -1,6 +1,7 @@ --- title: "ECS에서 EKS로 - kustomize + Gateway API 기반 운영 환경 구축기" date: "2026-03-12" +updated: "2026-03-12" description: "AWS ECS에서 EKS로 마이그레이션하면서 kustomize base/overlay 패턴, K8s Gateway API, Karpenter 노드 오토스케일링, Blue/Green 배포 전략까지 구축한 경험을 공유합니다." tags: ["kubernetes", "eks", "kustomize", "gateway-api", "karpenter", "devops", "aws"] series: "서비스 인프라 현대화" diff --git a/content/posts/ko/gitops-argocd-n8n-deployment.md b/content/posts/ko/gitops-argocd-n8n-deployment.md index 8c67aa5..b5bf9c0 100644 --- a/content/posts/ko/gitops-argocd-n8n-deployment.md +++ b/content/posts/ko/gitops-argocd-n8n-deployment.md @@ -1,6 +1,7 @@ --- title: "GitOps 자동화 배포 - ArgoCD + n8n + GitHub Actions로 Slack 승인 배포 파이프라인 구축하기" date: "2026-02-26" +updated: "2026-02-26" description: "ECS에서 EKS로 마이그레이션하면서 구축한 GitOps 기반 배포 자동화 시스템을 소개합니다. ArgoCD, n8n, GitHub Actions를 조합하여 환경별로 차등화된 배포 전략을 설계한 경험을 공유합니다." tags: ["kubernetes", "argocd", "gitops", "n8n", "devops", "github-actions"] draft: false diff --git a/content/posts/ko/hello-world.md b/content/posts/ko/hello-world.md index 6835a15..6bcce99 100644 --- a/content/posts/ko/hello-world.md +++ b/content/posts/ko/hello-world.md @@ -1,6 +1,7 @@ --- title: "Hello World" date: "2026-02-25" +updated: "2026-02-25" description: "첫 번째 블로그 포스트입니다." tags: ["blog"] draft: true diff --git a/content/posts/ko/i18n-01-foundation.md b/content/posts/ko/i18n-01-foundation.md index 8e9d5f0..4515358 100644 --- a/content/posts/ko/i18n-01-foundation.md +++ b/content/posts/ko/i18n-01-foundation.md @@ -1,6 +1,7 @@ --- title: "서비스 국제화 적용기 (1) - 하드코딩된 한국어를 걷어내며" date: "2026-05-19" +updated: "2026-05-19" description: "일본 진출을 앞두고 시작한 프론트엔드 국제화. 로케일 모델을 어떻게 정하고 메시지를 어떻게 관리하고 로케일을 온 시스템에 어떻게 흘려보냈는지 정리한다." tags: ["i18n", "nextjs", "react", "typescript"] series: "서비스 국제화 적용기" diff --git a/content/posts/ko/i18n-02-language-pack-gcs.md b/content/posts/ko/i18n-02-language-pack-gcs.md index 953cc4c..61fff66 100644 --- a/content/posts/ko/i18n-02-language-pack-gcs.md +++ b/content/posts/ko/i18n-02-language-pack-gcs.md @@ -1,6 +1,7 @@ --- title: "서비스 국제화 적용기 (2) - 언어팩 서빙: 번들, GCS 런타임 로드, 그리고 캐시" date: "2026-06-03" +updated: "2026-06-03" description: "번역을 앱과 함께 배포하면 문구 한 줄 고칠 때마다 배포가 필요하다. 언어팩을 GCS에서 런타임 로드하고 번들로 폴백한 구조, 그리고 배포해도 안 바뀌던 캐시 문제를 정리한다." tags: ["i18n", "nextjs", "gcs", "typescript"] series: "서비스 국제화 적용기" diff --git a/content/posts/ko/i18n-03-rollout-killswitch.md b/content/posts/ko/i18n-03-rollout-killswitch.md index 00fa79c..a1a80d3 100644 --- a/content/posts/ko/i18n-03-rollout-killswitch.md +++ b/content/posts/ko/i18n-03-rollout-killswitch.md @@ -1,6 +1,7 @@ --- title: "서비스 국제화 적용기 (3) - GrowthBook 킬스위치로 무중단 점진 롤아웃" date: "2026-06-17" +updated: "2026-06-17" description: "번역을 다 만들었어도 전원에게 한 번에 켜면 위험하다. 로케일 결정을 단일 차단점으로 모으고 GrowthBook 킬스위치로 i18n을 무중단·점진 롤아웃한 이야기." tags: ["i18n", "nextjs", "growthbook", "feature-flag"] series: "서비스 국제화 적용기" diff --git a/content/posts/ko/i18n-04-backend-locale.md b/content/posts/ko/i18n-04-backend-locale.md index 4e8509c..cee9e84 100644 --- a/content/posts/ko/i18n-04-backend-locale.md +++ b/content/posts/ko/i18n-04-backend-locale.md @@ -1,6 +1,7 @@ --- title: "서비스 국제화 적용기 (4) - 백엔드가 할 몫: 사용자 언어·시간대와 문서 로케일" date: "2026-06-26" +updated: "2026-06-26" description: "프론트만 번역하면 서버가 한국어 응답·문서로 다시 한국어를 섞는다. 백엔드가 사용자 언어·시간대를 저장하고 인증 컨텍스트로 들고 다니며 응답과 문서까지 로케일을 흘려보낸 이야기." tags: ["i18n", "spring-boot", "java", "backend"] series: "서비스 국제화 적용기" diff --git a/content/posts/ko/i18n-05-document-render.md b/content/posts/ko/i18n-05-document-render.md index fd6e0a3..e0b06cc 100644 --- a/content/posts/ko/i18n-05-document-render.md +++ b/content/posts/ko/i18n-05-document-render.md @@ -1,6 +1,7 @@ --- title: "서비스 국제화 적용기 (5) - 이미지로 박은 문서를 HTML로: 다국어 PDF 렌더링" date: "2026-07-04" +updated: "2026-07-04" description: "수강확인증·리포트는 오랫동안 언어가 이미지에 구워져 있었다. 이미지 기반 문서를 HTML 템플릿 + Cloud Run 렌더링으로 옮겨 '새 언어 = JSON 파일 하나'로 만든 아키텍처 전환기." tags: ["i18n", "cloud-run", "pdf", "weasyprint"] series: "서비스 국제화 적용기" diff --git a/content/posts/ko/jdk-migration-strategy-11-to-25.md b/content/posts/ko/jdk-migration-strategy-11-to-25.md index a3001dc..c169e88 100644 --- a/content/posts/ko/jdk-migration-strategy-11-to-25.md +++ b/content/posts/ko/jdk-migration-strategy-11-to-25.md @@ -1,6 +1,7 @@ --- title: "JDK 마이그레이션 (1) - JDK 11에서 21로, Spring Boot 3.x 전환기" date: "2026-03-18" +updated: "2026-03-18" description: "프로덕션 Spring Boot 서비스를 JDK 11 → 21로 업그레이드하며 겪은 javax→jakarta 전환, AWS SDK v3 마이그레이션, QueryDSL 호환성 해결 등 실전 경험을 정리합니다." tags: ["java", "jdk", "spring-boot", "migration", "devops"] series: "JDK Migration" diff --git a/content/posts/ko/jdk-migration-strategy-21-to-25.md b/content/posts/ko/jdk-migration-strategy-21-to-25.md index 7269a86..66d6d70 100644 --- a/content/posts/ko/jdk-migration-strategy-21-to-25.md +++ b/content/posts/ko/jdk-migration-strategy-21-to-25.md @@ -1,6 +1,7 @@ --- title: "JDK 마이그레이션 (2) - JDK 21에서 25로, Virtual Threads와 모던 Java" date: "2026-03-18" +updated: "2026-03-18" description: "JDK 21 → 25 업그레이드 실전기. Virtual Threads 활성화, pinning 이슈 해소, pattern matching switch 적용, Gradle Kotlin DSL 전환까지." tags: ["java", "jdk", "spring-boot", "virtual-threads", "devops"] series: "JDK Migration" diff --git a/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md b/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md index db10ff6..76e5cc4 100644 --- a/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md +++ b/content/posts/ko/jwt-hs256-to-rs256-jwks-kms.md @@ -1,6 +1,7 @@ --- title: "JWT 서명 전환 - HS256에서 RS256/JWKS/KMS로" date: "2026-04-24" +updated: "2026-04-24" description: "대칭키 JWT 서명을 RS256/JWKS/KMS 구조로 바꾸며 서명 책임, kid, key rotation, legacy 토큰 처리를 어떻게 나눴는지 정리합니다." tags: ["authentication", "jwt", "jwks", "kms", "java", "spring", "security"] draft: false diff --git a/content/posts/ko/lgtm-stack-observability.md b/content/posts/ko/lgtm-stack-observability.md index d85b4c9..38a018c 100644 --- a/content/posts/ko/lgtm-stack-observability.md +++ b/content/posts/ko/lgtm-stack-observability.md @@ -1,6 +1,7 @@ --- title: "Datadog 걷어내고 LGTM 스택 구축하기 - OTel Sidecar + Grafana/Loki/Tempo/Mimir" date: "2026-03-17" +updated: "2026-03-17" description: "Datadog 비용 압박으로 오픈소스 LGTM 스택(Grafana, Loki, Tempo, Mimir)으로 전환한 경험을 공유합니다. OTel Collector 사이드카 설계부터 노이즈 필터링, MDC 기반 요청 추적, IRSA 기반 S3 저장소 구성까지 실전 구축기입니다." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "observability", "devops"] series: "서비스 인프라 현대화" diff --git a/content/posts/ko/lgtm-stack-setup-guide.md b/content/posts/ko/lgtm-stack-setup-guide.md index 6af0188..8dd7f23 100644 --- a/content/posts/ko/lgtm-stack-setup-guide.md +++ b/content/posts/ko/lgtm-stack-setup-guide.md @@ -1,6 +1,7 @@ --- title: "EKS에 LGTM 스택 구축하기 - Loki, Grafana, Tempo, Mimir 실전 셋업 가이드" date: "2026-03-17" +updated: "2026-03-17" description: "EKS 클러스터에 Grafana LGTM 스택을 Helm으로 배포하고 OTel Collector 사이드카로 Spring Boot 앱의 로그·트레이스·메트릭을 수집하는 전체 과정을 다룹니다." tags: ["grafana", "loki", "tempo", "mimir", "opentelemetry", "kubernetes", "helm", "observability"] draft: false diff --git a/content/posts/ko/multi-channel-notification-server.md b/content/posts/ko/multi-channel-notification-server.md index 2704179..2977cb7 100644 --- a/content/posts/ko/multi-channel-notification-server.md +++ b/content/posts/ko/multi-channel-notification-server.md @@ -1,6 +1,7 @@ --- title: "멀티채널 알림 서버 구축기 - php 레거시에서 Java/Spring으로" date: "2026-03-09" +updated: "2026-03-09" description: "PHP 파일에 흩어져 있던 알림 로직을 Spring Boot 기반 독립 알림 서버로 구축한 과정. 카카오 알림톡, SMS, 이메일, 푸시, Slack까지 6개 채널을 하나의 서버에서 처리하는 구조를 만들기까지." tags: ["java", "spring-boot", "notification", "sqs", "architecture"] draft: false diff --git a/content/posts/ko/payment-system-migration-php-to-spring.md b/content/posts/ko/payment-system-migration-php-to-spring.md index 7741345..2a65033 100644 --- a/content/posts/ko/payment-system-migration-php-to-spring.md +++ b/content/posts/ko/payment-system-migration-php-to-spring.md @@ -1,6 +1,7 @@ --- title: "결제 시스템 재설계 (1) - PHP 레거시에서 Java/Spring으로" date: "2026-03-06" +updated: "2026-03-06" description: "PHP로 구현된 레거시 결제 시스템을 Java/Spring + SQS 기반 이벤트 드리븐 아키텍처로 전환한 과정을 공유합니다." tags: ["java", "spring-boot", "payment", "sqs", "migration"] series: "결제 시스템 재설계" diff --git a/content/posts/ko/payment-system-redesign-sync-api.md b/content/posts/ko/payment-system-redesign-sync-api.md index 703b407..b755c12 100644 --- a/content/posts/ko/payment-system-redesign-sync-api.md +++ b/content/posts/ko/payment-system-redesign-sync-api.md @@ -1,6 +1,7 @@ --- title: "결제 시스템 재설계 (2) - SQS를 걷어내고 동기 API로" date: "2026-03-06" +updated: "2026-03-06" description: "이벤트 드리븐의 매력에 빠져 도입한 SQS였지만 결제 도메인에서는 오히려 복잡도만 높였다. SQS를 제거하고 동기 API + Portone 웹훅 기반으로 전환하면서 결제 프로세스를 안정화한 이야기." tags: ["java", "spring-boot", "payment", "portone", "refactoring"] series: "결제 시스템 재설계" diff --git a/content/posts/ko/react-hooks-deep-dive.md b/content/posts/ko/react-hooks-deep-dive.md index 36c3bc0..baee805 100644 --- a/content/posts/ko/react-hooks-deep-dive.md +++ b/content/posts/ko/react-hooks-deep-dive.md @@ -1,6 +1,7 @@ --- title: "React Hooks 딥다이브" date: "2026-02-26" +updated: "2026-02-26" description: "React Hooks의 동작 원리와 실전 패턴을 깊이 있게 살펴봅니다." tags: ["react", "hooks", "typescript"] series: "React 마스터하기" diff --git a/content/posts/ko/react-typescript-tips.md b/content/posts/ko/react-typescript-tips.md index b2b4c57..edf6b5d 100644 --- a/content/posts/ko/react-typescript-tips.md +++ b/content/posts/ko/react-typescript-tips.md @@ -1,6 +1,7 @@ --- title: "React + TypeScript 실전 팁" date: "2026-02-20" +updated: "2026-02-20" description: "React 프로젝트에서 TypeScript를 효과적으로 사용하는 팁들을 정리합니다." tags: ["react", "typescript"] series: "React 마스터하기" diff --git a/content/posts/ko/spring-ai-cs-automation.md b/content/posts/ko/spring-ai-cs-automation.md index 4510aef..f37d9c1 100644 --- a/content/posts/ko/spring-ai-cs-automation.md +++ b/content/posts/ko/spring-ai-cs-automation.md @@ -1,6 +1,7 @@ --- title: "Spring AI로 CS 자동화 시스템 구축하기 - Function Calling과 FAQ 지침서" date: "2026-07-06" +updated: "2026-07-06" description: "반복되는 고객 문의를 Spring AI 기반으로 자동화한 경험. Function Calling으로 사용자 맥락을 조회하고 FAQ를 지침서로 등록해 자동 답변과 상담 초안 추천을 함께 구성한 이야기." tags: ["spring-ai", "spring-boot", "ai", "llm", "cs"] draft: false diff --git a/content/posts/ko/spring-ai-guide-01-setup.md b/content/posts/ko/spring-ai-guide-01-setup.md index 6e2e9c3..4770679 100644 --- a/content/posts/ko/spring-ai-guide-01-setup.md +++ b/content/posts/ko/spring-ai-guide-01-setup.md @@ -1,6 +1,7 @@ --- title: "Spring AI 적용 가이드 (1) - 프로젝트 설정부터 첫 번째 AI 호출까지" date: "2026-03-17" +updated: "2026-03-17" description: "Spring AI의 핵심 개념과 프로젝트 설정, ChatClient를 이용한 첫 번째 AI 호출까지를 정리합니다." tags: ["spring-ai", "spring-boot", "ai", "llm", "java"] series: "Spring AI 적용 가이드" diff --git a/content/posts/ko/spring-ai-guide-02-multi-provider.md b/content/posts/ko/spring-ai-guide-02-multi-provider.md index dbc7921..6b8c728 100644 --- a/content/posts/ko/spring-ai-guide-02-multi-provider.md +++ b/content/posts/ko/spring-ai-guide-02-multi-provider.md @@ -1,6 +1,7 @@ --- title: "Spring AI 적용 가이드 (2) - 멀티 프로바이더 전략" date: "2026-03-17" +updated: "2026-03-17" description: "OpenAI, AWS Bedrock, Google Gemini를 하나의 ChatClient 인터페이스로 추상화하고 런타임에 동적으로 전환하는 방법을 다룹니다." tags: ["spring-ai", "spring-boot", "ai", "openai", "bedrock", "gemini"] series: "Spring AI 적용 가이드" diff --git a/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md b/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md index 2c8fd15..7d9bc07 100644 --- a/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md +++ b/content/posts/ko/spring-ai-guide-03-prompt-structured-output.md @@ -1,6 +1,7 @@ --- title: "Spring AI 적용 가이드 (3) - 프롬프트 관리 & Structured Output" date: "2026-03-17" +updated: "2026-03-17" description: "Spring AI의 메시지 모델, 변수 템플릿링, DB 기반 프롬프트 관리, JSON Schema를 활용한 Structured Output을 다룹니다." tags: ["spring-ai", "spring-boot", "ai", "prompt-engineering", "structured-output"] series: "Spring AI 적용 가이드" diff --git a/content/posts/ko/spring-ai-pipeline-real-world.md b/content/posts/ko/spring-ai-pipeline-real-world.md index d0dee54..0c74999 100644 --- a/content/posts/ko/spring-ai-pipeline-real-world.md +++ b/content/posts/ko/spring-ai-pipeline-real-world.md @@ -1,6 +1,7 @@ --- title: "Spring AI 실전 적용기 - 7단계 AI 진단 파이프라인 구축" date: "2026-02-28" +updated: "2026-02-28" description: "HTTP 직접 호출 + 동기 단일 프롬프트 방식을 Spring AI 기반 7단계 파이프라인으로 재설계한 실전 경험을 공유합니다." tags: ["spring-ai", "spring-boot", "ai", "llm"] draft: false diff --git a/content/posts/ko/spring-facade-pattern-layered-architecture.md b/content/posts/ko/spring-facade-pattern-layered-architecture.md index 93604f5..4adba50 100644 --- a/content/posts/ko/spring-facade-pattern-layered-architecture.md +++ b/content/posts/ko/spring-facade-pattern-layered-architecture.md @@ -1,6 +1,7 @@ --- title: "Spring MVC에 Facade 패턴 도입기 - @Gateway 어노테이션으로 레이어드 아키텍처 개선" date: "2026-03-10" +updated: "2026-03-10" description: "전통적인 Controller-Service-Repository 구조에 커스텀 @Gateway 어노테이션과 Facade 레이어를 도입하여 도메인별 단일 책임 원칙을 확보하고 팀 생산성을 높인 과정을 공유합니다." tags: ["java", "spring-boot", "architecture", "design-pattern", "facade"] draft: false diff --git a/content/posts/ko/sync-virtual-coroutine-benchmark.md b/content/posts/ko/sync-virtual-coroutine-benchmark.md index 0c8d8e6..5df6f18 100644 --- a/content/posts/ko/sync-virtual-coroutine-benchmark.md +++ b/content/posts/ko/sync-virtual-coroutine-benchmark.md @@ -1,6 +1,7 @@ --- title: "Platform Thread vs Virtual Thread vs Coroutine - 10,000 태스크 벤치마크" date: "2026-03-24" +updated: "2026-03-24" description: "JDK 25 Platform Thread, Virtual Thread, Kotlin Coroutine의 I/O Bound, CPU Bound, High Concurrency 시나리오별 성능을 100회 반복 벤치마크로 비교한다." tags: ["java", "kotlin", "virtual-threads", "coroutine", "benchmark", "concurrency"] draft: false diff --git a/content/posts/ko/tailwind-css-guide.md b/content/posts/ko/tailwind-css-guide.md index 00c98a3..ccdc5df 100644 --- a/content/posts/ko/tailwind-css-guide.md +++ b/content/posts/ko/tailwind-css-guide.md @@ -1,6 +1,7 @@ --- title: "Tailwind CSS 시작하기" date: "2026-02-15" +updated: "2026-02-15" description: "Tailwind CSS의 기본 개념과 활용법을 알아봅니다." tags: ["css", "tailwind"] draft: true diff --git a/docs/superpowers/plans/2026-07-19-sitemap-lastmod.md b/docs/superpowers/plans/2026-07-19-sitemap-lastmod.md new file mode 100644 index 0000000..d7e10d8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-19-sitemap-lastmod.md @@ -0,0 +1,75 @@ +# Sitemap 최종 수정일 구현 계획 + +**목표:** 기존 `date` 값은 유지하면서 모든 게시글에 같은 값의 필수 `updated`를 추가하고, sitemap·JSON-LD·화면에 일관되게 반영한다. 신뢰할 수 없는 고정/집계 페이지 빌드 날짜는 sitemap에서 제거한다. + +## 1. 날짜 정책을 테스트로 고정 + +**파일** + +- 추가: `src/lib/post-dates.ts` +- 추가: `scripts/post-dates.test.ts` +- 수정: `package.json` + +**절차** + +1. 필수 `updated`, 과거 `updated`, 미래 `updated`, 잘못된 날짜 형식을 검증하는 테스트를 먼저 작성한다. +2. 테스트가 구현 모듈 부재로 실패하는지 확인한다. +3. 날짜 선택·검증 순수 함수를 최소 구현한다. +4. Node 내장 테스트가 통과하는지 확인한다. + +## 2. 게시글 메타데이터에 updated 추가 + +**파일** + +- 수정: `src/types/post.ts` +- 수정: `src/lib/posts.ts` +- 수정: `vite.config.ts` + +**절차** + +1. `PostMeta`와 빌드용 게시글 타입에 필수 `updated`를 추가한다. +2. 런타임/빌드 frontmatter 파서가 `updated`를 읽도록 한다. +3. 빌드 단계에서 게시일과 수정일 정책을 검증해 잘못된 콘텐츠가 배포되지 않게 한다. +4. 기존 정렬과 RSS 게시일은 `date` 기준으로 유지한다. + +## 3. sitemap 출력 수정 + +**파일** + +- 수정: `vite.config.ts` +- 추가: `scripts/sitemap-lastmod.test.mjs` + +**절차** + +1. URL 엔트리가 `lastmod`를 선택적으로 출력하도록 바꾼다. +2. 게시글 URL은 `updated`를 출력한다. +3. 홈·목록·태그·시리즈·통계·소개·개인정보 페이지는 `lastmod`를 생략한다. +4. 빌드 결과의 모든 공개 게시글과 고정 페이지 정책을 검사하는 회귀 테스트를 추가한다. + +## 4. JSON-LD와 화면 표시 일치 + +**파일** + +- 수정: `src/entry-server.tsx` +- 수정: `src/pages/PostPage.tsx` +- 수정: `src/i18n/translations.ts` + +**절차** + +1. 게시글 JSON-LD의 `dateModified`를 `updated`로 출력한다. +2. `datePublished`는 기존 `date`를 유지한다. +3. `updated`가 실제 게시일과 다를 때만 한국어/영어 수정일 문구를 표시한다. + +## 5. 전체 검증 및 인계 + +**명령** + +- `npm run test:post-dates` +- `npm run build` +- `npm run test:sitemap-lastmod` +- `npm run check:internal-links` +- `npm run type-check` +- `npm run lint` +- `git diff --check` + +빌드된 sitemap과 대표 게시글 HTML을 직접 검사해 ``, `datePublished`, `dateModified`가 설계와 일치하는지 확인한다. Search Console 재제출·재검증은 변경 사항이 실제 배포된 뒤 별도 승인 하에 수행한다. diff --git a/docs/superpowers/specs/2026-07-19-sitemap-lastmod-design.md b/docs/superpowers/specs/2026-07-19-sitemap-lastmod-design.md new file mode 100644 index 0000000..455f65e --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-sitemap-lastmod-design.md @@ -0,0 +1,43 @@ +# Sitemap 최종 수정일 설계 + +## 목표 + +게시글 sitemap의 `lastmod`를 frontmatter의 명시적인 최종 수정일로 관리한다. 기존 `date` 값은 변경하지 않고, 모든 게시글에 초기 `updated`를 같은 값으로 추가한다. 앞으로 본문·구조화 데이터·주요 링크가 바뀐 글만 `updated`를 변경한다. + +## 데이터 모델 + +- `date`: 최초 게시일이며 기존 값을 변경하지 않는다. +- `updated`: 필수 필드다. 최종 수정일을 `YYYY-MM-DD`로 기록한다. +- 처음 추가할 때는 기존 `date`와 같은 값을 사용하고, 이후 의미 있는 수정이 있을 때만 변경한다. +- `updated`는 `date`보다 빠르거나 미래일 수 없다. + +기존 게시글 73개에는 현재 `date`와 동일한 `updated`를 명시적으로 추가한다. 따라서 현재 공개 게시글의 sitemap 날짜 값은 바뀌지 않는다. + +## 출력 정책 + +- 게시글 sitemap ``: `updated`. +- 게시글 JSON-LD: `datePublished`에는 `date`, `dateModified`에는 `updated`. +- 게시글 화면: `updated`가 있고 `updated !== date`일 때만 수정일을 표시한다. +- 홈·글 목록·태그·시리즈·통계·소개·개인정보처리방침 같은 집계/고정 페이지: 신뢰할 수 없는 빌드 날짜를 넣지 않고 ``를 생략한다. +- RSS의 ``는 게시 시점을 뜻하므로 기존 `date`를 유지한다. + +## 구현 경계 + +- 게시글 타입과 두 frontmatter 파서(`src/lib/posts.ts`, `vite.config.ts`)가 `updated`를 읽는다. +- sitemap 생성기는 URL 항목별로 `lastmod`를 선택적으로 출력한다. +- 서버 프리렌더 메타데이터와 JSON-LD에 `updated`를 전달한다. +- 기존 정렬은 게시일 기준을 유지한다. 수정된 글이 최신 글로 다시 올라오지 않는다. + +## 검증 + +- 순수 날짜 선택/검증 로직을 Node 내장 테스트로 검증한다. +- 빌드 후 공개 게시글 URL의 ``가 `updated`인지 확인한다. +- 고정 페이지에 ``가 없는지 확인한다. +- 프리렌더 HTML의 `datePublished`와 `dateModified`를 확인한다. +- 기존 내부 링크 검사, 타입 검사, 린트, 전체 빌드를 실행한다. + +## 비범위 + +- Git 커밋 시각이나 파일 시스템 mtime으로 수정일 자동 추론 +- 기존 `date` 값 변경 또는 Git 이력 기반 수정일 추정 +- Search Console의 의도된 NOINDEX 또는 리디렉션 정책 변경 diff --git a/package.json b/package.json index 1312dd9..bca3e7f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "preview": "vite preview", "lint": "eslint .", "type-check": "tsc --noEmit", - "check:internal-links": "node scripts/check-internal-post-links.mjs" + "check:internal-links": "node scripts/check-internal-post-links.mjs", + "test:post-dates": "node --test scripts/post-dates.test.mjs", + "test:sitemap-lastmod": "node --test scripts/sitemap-lastmod.test.mjs" }, "dependencies": { "@giscus/react": "^3.1.0", diff --git a/scripts/post-dates.test.mjs b/scripts/post-dates.test.mjs new file mode 100644 index 0000000..6befe60 --- /dev/null +++ b/scripts/post-dates.test.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict" +import test from "node:test" + +import { + assertValidPostDates, + getPostModifiedDate, + hasDistinctUpdatedDate, +} from "../src/lib/post-dates.ts" + +test("requires an explicit updated date", () => { + assert.throws( + () => assertValidPostDates({ date: "2026-07-01" }, "2026-07-19"), + /updated date.*required/, + ) +}) + +test("uses and exposes a distinct updated date", () => { + const dates = { date: "2026-07-01", updated: "2026-07-10" } + + assert.equal(getPostModifiedDate(dates), "2026-07-10") + assert.equal(hasDistinctUpdatedDate(dates), true) +}) + +test("treats an updated date equal to the publication date as unchanged", () => { + const dates = { date: "2026-07-01", updated: "2026-07-01" } + + assert.equal(getPostModifiedDate(dates), "2026-07-01") + assert.equal(hasDistinctUpdatedDate(dates), false) +}) + +test("rejects invalid calendar dates", () => { + assert.throws( + () => assertValidPostDates({ date: "2026-02-30", updated: "2026-02-30" }, "2026-07-19"), + /publication date.*YYYY-MM-DD/, + ) +}) + +test("rejects an updated date before publication", () => { + assert.throws( + () => assertValidPostDates({ date: "2026-07-10", updated: "2026-07-09" }, "2026-07-19"), + /before publication date/, + ) +}) + +test("rejects a future updated date", () => { + assert.throws( + () => assertValidPostDates({ date: "2026-07-10", updated: "2026-07-20" }, "2026-07-19"), + /future/, + ) +}) diff --git a/scripts/sitemap-lastmod.test.mjs b/scripts/sitemap-lastmod.test.mjs new file mode 100644 index 0000000..bd9c0e4 --- /dev/null +++ b/scripts/sitemap-lastmod.test.mjs @@ -0,0 +1,93 @@ +import assert from "node:assert/strict" +import fs from "node:fs" +import path from "node:path" +import test from "node:test" +import { fileURLToPath } from "node:url" + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const baseUrl = "https://devy1540.dev" +const today = new Date().toISOString().slice(0, 10) + +function frontmatterValue(raw, key) { + const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "" + const line = frontmatter.split(/\r?\n/).find((candidate) => candidate.startsWith(`${key}:`)) + return line?.slice(line.indexOf(":") + 1).trim().replace(/^['"]|['"]$/g, "") ?? "" +} + +function readPublishedPosts(language) { + const postsDir = path.join(rootDir, "content/posts", language) + + return fs.readdirSync(postsDir) + .filter((file) => file.endsWith(".md")) + .map((file) => { + const raw = fs.readFileSync(path.join(postsDir, file), "utf8") + return { + slug: file.slice(0, -3), + language, + date: frontmatterValue(raw, "date"), + updated: frontmatterValue(raw, "updated"), + draft: frontmatterValue(raw, "draft") === "true", + publishDate: frontmatterValue(raw, "publishDate"), + } + }) + .filter((post) => !post.draft && !(post.publishDate && post.publishDate > today)) +} + +function parseSitemapEntries() { + const sitemap = fs.readFileSync(path.join(rootDir, "dist/sitemap.xml"), "utf8") + return [...sitemap.matchAll(/([\s\S]*?)<\/url>/g)].map((match) => ({ + loc: match[1].match(/(.*?)<\/loc>/)?.[1], + lastmod: match[1].match(/(.*?)<\/lastmod>/)?.[1], + })) +} + +function postUrl(post) { + const prefix = post.language === "en" ? "/en" : "" + return `${baseUrl}${prefix}/posts/${post.slug}/` +} + +function readBlogPostingJsonLd(post) { + const prefix = post.language === "en" ? ["en"] : [] + const htmlPath = path.join(rootDir, "dist", ...prefix, "posts", post.slug, "index.html") + const html = fs.readFileSync(htmlPath, "utf8") + const documents = [...html.matchAll(/