Skip to content

Commit 84b7821

Browse files
committed
fix(content): 감사 정책 기준 공유
Node 기반 content audit이 앱 정책과 같은 JSON 기준을 사용하도록 변경했습니다. 공개 Tech와 featured 검증·요약의 기준 불일치를 방지했습니다.
1 parent 0f3d806 commit 84b7821

7 files changed

Lines changed: 129 additions & 24 deletions

File tree

blog/services/policy.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"publicTech": {
3+
"coreReviewFields": ["philosophy", "design", "implementation"],
4+
"minimumCoreReviewAverageExclusive": 3
5+
},
6+
"featured": {
7+
"category": "Tech",
8+
"minimumBrandFit": 4,
9+
"requiresNoSeries": true
10+
}
11+
}

blog/services/policy.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
11
import type { FeedData, QualityReview } from '@/blog/model/types';
2+
import policyData from './policy.json';
23

3-
export const PUBLICATION_POLICY = {
4-
publicTech: {
5-
minimumCoreReviewAverageExclusive: 3,
6-
},
7-
featured: {
8-
category: 'Tech',
9-
minimumBrandFit: 4,
10-
},
11-
} as const;
4+
export const PUBLICATION_POLICY = policyData;
125

13-
const CORE_TECH_REVIEW_FIELDS = [
14-
'philosophy',
15-
'design',
16-
'implementation',
17-
] as const;
6+
const CORE_TECH_REVIEW_FIELDS = PUBLICATION_POLICY.publicTech
7+
.coreReviewFields as ReadonlyArray<keyof QualityReview>;
188

199
export interface PublicationQueryOptions {
2010
includePrivate?: boolean;
@@ -69,7 +59,7 @@ export function isEligibleForFeaturedPost(
6959

7060
return (
7161
post.category === PUBLICATION_POLICY.featured.category &&
72-
!post.series &&
62+
(!PUBLICATION_POLICY.featured.requiresNoSeries || !post.series) &&
7363
typeof brandFit === 'number' &&
7464
brandFit >= PUBLICATION_POLICY.featured.minimumBrandFit
7565
);

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Last updated: 2026-07-22
4747
- `docs/adr/0046-use-main-as-the-primary-branch.md`
4848
- `docs/adr/0047-isolate-publication-policy.md`
4949
- `docs/adr/0048-centralize-blog-publication-rules.md`
50+
- `docs/adr/0049-share-publication-policy-data-with-audit.md`
5051
- `docs/blog-quality-guide.md`
5152
- `docs/content-publication-candidates.md`
5253
- `docs/database/db-schema.md`
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# 0049. 발행 정책 데이터를 content audit과 공유한다
2+
3+
Date: 2026-07-22
4+
Status: Accepted
5+
6+
## 배경
7+
8+
`blog/services/policy.ts`는 앱과 테스트의 발행 기준을 소유하지만,
9+
`content:audit`은 Node가 직접 실행하는 `.mjs` 스크립트다. 이 스크립트에 core
10+
점수 하한과 featured 기준을 다시 쓰면 정책 변경 후 release check와 앱 검증이
11+
서로 다른 결과를 낼 수 있다.
12+
13+
## 결정
14+
15+
- `blog/services/policy.json`을 Node와 TypeScript가 함께 읽는 발행 정책 데이터의
16+
원본으로 둔다.
17+
- `policy.ts`는 JSON을 import하고 visibility, 점수, featured 자격 helper를
18+
제공한다.
19+
- `content:audit`은 같은 JSON을 import해 core 평가 항목, 점수 하한, featured
20+
자격과 요약을 계산한다.
21+
22+
## 결과
23+
24+
- 정책 기준을 바꾸면 앱, policy test, content audit이 같은 값을 사용한다.
25+
- Node audit에 TypeScript runtime loader를 추가하지 않는다.
26+
- 글별 featured 선택은 계속 `meta.json`의 책임이다.
27+
28+
## 검토한 대안
29+
30+
- audit에 숫자를 유지: 정책 변경 시 기준 불일치가 다시 생긴다.
31+
- audit에서 `policy.ts`를 직접 import: plain Node 실행에 TypeScript loader가
32+
필요하다.
33+
- JSON 대신 별도 build 산출물 생성: 현재 한 정책 파일에는 불필요한 build 단계다.
34+
35+
## Related History
36+
37+
- `0f3d806`: 발행 정책을 `policy.ts`로 중앙화

docs/adr/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ ADR은 AI 협업 가이드와 별개의 문서다. 사람이 결정했든 AI가
6666
| [0046](0046-use-main-as-the-primary-branch.md) | Accepted | 저장소의 주 브랜치를 main으로 통일한다 |
6767
| [0047](0047-isolate-publication-policy.md) | Accepted | 공개 정책을 blog 도메인 모듈로 분리한다 |
6868
| [0048](0048-centralize-blog-publication-rules.md) | Accepted | 블로그 발행 규칙을 단일 policy 모듈로 확장한다 |
69+
| [0049](0049-share-publication-policy-data-with-audit.md) | Accepted | 발행 정책 데이터를 content audit과 공유한다 |
6970

7071
## 작성 조건
7172

tooling/scripts/posts/audit-post-quality.mjs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import process from 'node:process';
4+
import publicationPolicy from '../../../blog/services/policy.json' with { type: 'json' };
45

56
const configuredPostsDirectory = process.env.BLOG_AUDIT_POSTS_DIR ?? 'posts';
67
const postsDirectory = path.isAbsolute(configuredPostsDirectory)
78
? configuredPostsDirectory
89
: path.join(process.cwd(), configuredPostsDirectory);
910
const CONTENT_TYPES = ['essay', 'retrospective', 'review'];
10-
const CORE_REVIEW_KEYS = ['philosophy', 'design', 'implementation'];
11+
const CORE_REVIEW_KEYS = publicationPolicy.publicTech.coreReviewFields;
1112
const WRITING_REVIEW_KEYS = [
1213
'clarity',
1314
'structure',
@@ -292,6 +293,8 @@ function reviewPostQuality(meta, content) {
292293
function evaluatePolicy(meta) {
293294
const visibility = meta.visibility ?? 'private';
294295
const coreAverage = readCoreAverage(meta.qualityReview);
296+
const publicTechPolicy = publicationPolicy.publicTech;
297+
const featuredPolicy = publicationPolicy.featured;
295298
const warnings = [];
296299

297300
if (visibility === 'public' && meta.series) {
@@ -301,16 +304,33 @@ function evaluatePolicy(meta) {
301304
if (visibility === 'public' && meta.category === 'Tech') {
302305
if (coreAverage === null) {
303306
warnings.push('public Tech core 점수 누락');
304-
} else if (coreAverage <= 3) {
305-
warnings.push(`public Tech core 평균 ${coreAverage.toFixed(2)} <= 3.0`);
307+
} else if (
308+
coreAverage <= publicTechPolicy.minimumCoreReviewAverageExclusive
309+
) {
310+
warnings.push(
311+
`public Tech core 평균 ${coreAverage.toFixed(2)} <= ${publicTechPolicy.minimumCoreReviewAverageExclusive.toFixed(1)}`
312+
);
306313
}
307314
}
308315

309316
if (meta.featured) {
310317
const brandFit = meta.qualityReview?.brandFit;
311318

312-
if (typeof brandFit !== 'number' || brandFit < 4) {
313-
warnings.push('featured brandFit < 4.0');
319+
if (meta.category !== featuredPolicy.category) {
320+
warnings.push(`featured category must be ${featuredPolicy.category}`);
321+
}
322+
323+
if (featuredPolicy.requiresNoSeries && meta.series) {
324+
warnings.push('featured series 글');
325+
}
326+
327+
if (
328+
typeof brandFit !== 'number' ||
329+
brandFit < featuredPolicy.minimumBrandFit
330+
) {
331+
warnings.push(
332+
`featured brandFit < ${featuredPolicy.minimumBrandFit.toFixed(1)}`
333+
);
314334
}
315335
}
316336

@@ -602,20 +622,27 @@ function printReport(posts, errors) {
602622

603623
console.log('## Policy Checks');
604624
console.log(
605-
`- public Tech core average > 3.0: ${
625+
`- public Tech core average > ${publicationPolicy.publicTech.minimumCoreReviewAverageExclusive.toFixed(1)}: ${
606626
publicTechPosts.every(
607627
(post) =>
608-
post.policy.coreAverage !== null && post.policy.coreAverage > 3
628+
post.policy.coreAverage !== null &&
629+
post.policy.coreAverage >
630+
publicationPolicy.publicTech.minimumCoreReviewAverageExclusive
609631
)
610632
? 'ok'
611633
: 'review'
612634
}`
613635
);
614636
console.log(
615-
`- featured brandFit >= 4.0: ${
637+
`- featured eligibility: ${
616638
featuredPosts.every((post) => {
617639
const brandFit = post.qualityReview?.brandFit;
618-
return typeof brandFit === 'number' && brandFit >= 4;
640+
return (
641+
post.category === publicationPolicy.featured.category &&
642+
(!publicationPolicy.featured.requiresNoSeries || !post.series) &&
643+
typeof brandFit === 'number' &&
644+
brandFit >= publicationPolicy.featured.minimumBrandFit
645+
);
619646
})
620647
? 'ok'
621648
: 'review'

tooling/scripts/posts/audit-post-quality.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from 'node:fs';
33
import os from 'node:os';
44
import path from 'node:path';
55
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
import { PUBLICATION_POLICY } from '@/blog/services/policy';
67

78
let tempRoot: string;
89
let postsDir: string;
@@ -160,4 +161,41 @@ describe('post quality audit script', () => {
160161
'[private 유지] missing-visibility (private, Life)'
161162
);
162163
});
164+
165+
it('uses the shared publication thresholds for warnings and summaries', () => {
166+
const coreThreshold =
167+
PUBLICATION_POLICY.publicTech.minimumCoreReviewAverageExclusive;
168+
const featuredThreshold = PUBLICATION_POLICY.featured.minimumBrandFit;
169+
170+
writePostFixture('thresholds', {
171+
title: '공용 공개 기준을 검증하는 글',
172+
slug: 'thresholds',
173+
description: '설명',
174+
date: '2026-07-22',
175+
category: PUBLICATION_POLICY.featured.category,
176+
contentType: 'retrospective',
177+
visibility: 'public',
178+
featured: true,
179+
tags: ['Policy'],
180+
qualityReview: {
181+
philosophy: coreThreshold,
182+
design: coreThreshold,
183+
implementation: coreThreshold,
184+
brandFit: featuredThreshold - 0.5,
185+
},
186+
});
187+
188+
const output = runAudit();
189+
190+
expect(output).toContain(
191+
`public Tech core 평균 ${coreThreshold.toFixed(2)} <= ${coreThreshold.toFixed(1)}`
192+
);
193+
expect(output).toContain(
194+
`featured brandFit < ${featuredThreshold.toFixed(1)}`
195+
);
196+
expect(output).toContain(
197+
`public Tech core average > ${coreThreshold.toFixed(1)}: review`
198+
);
199+
expect(output).toContain('- featured eligibility: review');
200+
});
163201
});

0 commit comments

Comments
 (0)