diff --git a/.github/workflows/translate-review.yml b/.github/workflows/translate-review.yml new file mode 100644 index 000000000..f3714c42a --- /dev/null +++ b/.github/workflows/translate-review.yml @@ -0,0 +1,243 @@ +name: Translation Review (GitHub Models) + +on: + pull_request: + types: [opened, synchronize] + paths: + - "src/content/**/*.mdx" + +permissions: + contents: read + pull-requests: write + models: read + +jobs: + review: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed MDX files + id: changed + run: | + FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep '\.mdx$' || true) + if [ -z "$FILES" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "No MDX files changed." + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "$FILES" > /tmp/changed-files.txt + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Save PR diff to file + if: steps.changed.outputs.skip == 'false' + run: | + # 일반 diff (라인 번호 파악용) + gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-diff.txt + + # word-diff (단어 단위 변경점 파악용) + # [-삭제된 단어-] {+추가된 단어+} 형식으로 표시 + BASE_SHA=$(gh pr view ${{ github.event.pull_request.number }} --json baseRefName -q '.baseRefName') + git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || true + git diff --word-diff origin/"$BASE_SHA"..HEAD -- 'src/content/**/*.mdx' > /tmp/pr-word-diff.txt || \ + gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-word-diff.txt + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Review with GitHub Models + if: steps.changed.outputs.skip == 'false' + env: + GH_MODELS_TOKEN: ${{ secrets.GH_MODELS_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -Eeuo pipefail + + DIFF=$(cat /tmp/pr-diff.txt) + WORD_DIFF=$(cat /tmp/pr-word-diff.txt) + AGENTS=$(cat AGENTS.md) + + read -r -d '' SYSTEM_PROMPT << 'SYSPROMPT' || true + 당신은 React Hook Form 한국어 번역 프로젝트의 번역 품질 리뷰어입니다. + 반드시 한국어로 응답하세요. + + ## 번역 규칙 + PLACEHOLDER_AGENTS + + ## Word-Diff 읽는 법 + - `[-삭제된 텍스트-]`: 이전 버전 + - `{+추가된 텍스트+}`: 새 버전 + - 예: `[-수동으로-]{+직접+}` = "수동으로"가 "직접"으로 변경됨 + + ## 리뷰 방법 + + **각 변경에 대해 다음을 분석하세요:** + + 1. **변경 내용 나열**: word-diff에서 모든 `[-...-]{+...+}` 쌍을 찾아 나열 + 2. **변경 이유 추론**: 왜 이렇게 변경했는지 파악 + 3. **적절성 판단**: + - 번역 규칙에 맞는가? + - 더 자연스러운 한국어인가? + - 원래 의미를 잘 전달하는가? + 4. **결론**: 좋은 변경인지, 불필요한 변경인지, 오히려 나빠진 변경인지 + + ## 응답 형식 (JSON) + + { + "summary": "전체 요약 (1-2문장)", + "comments": [ + { + "path": "src/content/.../파일.mdx", + "line": 변경된 라인 번호 (숫자), + "body": "**변경 내용:**\n- `이전` → `이후`\n- `이전2` → `이후2`\n\n**평가:** 좋은/나쁜 변경인 이유를 구체적으로 설명" + } + ] + } + + ## 코멘트 기준 (실제 리뷰어처럼) + + **코멘트를 다는 경우:** + - 번역 규칙 위반 (용어표 미준수) + - 오역 또는 의미 왜곡 + - 어색한 번역투 + - 정말 잘한 변경 (구체적 이유와 함께 칭찬) + + **코멘트를 달지 않는 경우:** + - 변경이 납득됨, 이해됨, 괜찮음 → 굳이 안 달아도 됨 + - summary에서 간단히 언급하면 충분 + + ## 필수 규칙 + - 코멘트의 body에는 반드시: + 1. 변경 내용 (`이전` → `이후` 형식) + 2. 왜 문제인지 / 왜 좋은지 구체적 이유 + - "좋아 보입니다" 같은 애매한 표현 금지. 구체적 근거 필수. + - 워크플로우(.yml) 파일 변경은 무시하세요 + - 문제 없으면 comments를 빈 배열 []로 하세요 + SYSPROMPT + + SYSTEM_PROMPT="${SYSTEM_PROMPT//PLACEHOLDER_AGENTS/$AGENTS}" + + USER_MSG="다음 PR의 번역 변경사항을 리뷰하고 JSON 형식으로 응답하세요. + + ## Word Diff (단어 단위 변경점) + [-삭제-]와 {+추가+} 표시를 확인하여 정확히 무엇이 변경되었는지 파악하세요. + + \`\`\`diff + $WORD_DIFF + \`\`\` + + ## 일반 Diff (라인 번호 참조용) + 인라인 코멘트의 line 번호는 이 diff를 참고하세요. + + \`\`\`diff + $DIFF + \`\`\`" + + PAYLOAD=$(jq -n \ + --arg model "openai/gpt-4o" \ + --arg system "$SYSTEM_PROMPT" \ + --arg user "$USER_MSG" \ + '{model: $model, messages: [ + {role: "system", content: $system}, + {role: "user", content: $user} + ], max_tokens: 4096, response_format: {type: "json_object"}}') + + echo "Calling GitHub Models API..." + + HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" --max-time 120 \ + "https://models.github.ai/inference/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $GH_MODELS_TOKEN" \ + -d "$PAYLOAD") + + HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -n1) + RESPONSE=$(echo "$HTTP_RESPONSE" | sed '$d') + + echo "HTTP Status: $HTTP_STATUS" + + if [ "$HTTP_STATUS" != "200" ]; then + echo "API Error Response: $RESPONSE" + echo "::error::GitHub Models API returned status $HTTP_STATUS" + exit 1 + fi + + REVIEW_JSON=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // empty') + + if [ -z "$REVIEW_JSON" ]; then + echo "Response body: $RESPONSE" + echo "::error::No review content in response" + exit 1 + fi + + echo "Review JSON: $REVIEW_JSON" + + INPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.usage.prompt_tokens // 0') + OUTPUT_TOKENS=$(echo "$RESPONSE" | jq -r '.usage.completion_tokens // 0') + echo "Tokens used - input: $INPUT_TOKENS, output: $OUTPUT_TOKENS" + + SUMMARY=$(echo "$REVIEW_JSON" | jq -r '.summary // "요약 없음"') + GOOD_POINTS=$(echo "$REVIEW_JSON" | jq -r '.good_points // ""') + COMMENTS_COUNT=$(echo "$REVIEW_JSON" | jq '.comments | length') + + echo "Summary: $SUMMARY" + echo "Good points: $GOOD_POINTS" + echo "Comments count: $COMMENTS_COUNT" + + if [ "$COMMENTS_COUNT" -gt 0 ]; then + echo "Creating PR review with inline comments..." + + REVIEW_COMMENTS=$(echo "$REVIEW_JSON" | jq '[.comments[] | {path: .path, line: .line, body: .body}]') + + REVIEW_BODY="## 번역 리뷰 (GitHub Models) + + ### 요약 + $SUMMARY + + ### 잘된 점 + ${GOOD_POINTS:-"특별히 언급할 사항 없음"} + + --- + GPT-4o via GitHub Models | tokens: ${INPUT_TOKENS} in / ${OUTPUT_TOKENS} out | 인라인 코멘트: ${COMMENTS_COUNT}개" + + REVIEW_PAYLOAD=$(jq -n \ + --arg body "$REVIEW_BODY" \ + --arg commit_id "$HEAD_SHA" \ + --arg event "COMMENT" \ + --argjson comments "$REVIEW_COMMENTS" \ + '{body: $body, commit_id: $commit_id, event: $event, comments: $comments}') + + echo "Submitting review..." + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + "/repos/$REPO/pulls/$PR_NUMBER/reviews" \ + --input - <<< "$REVIEW_PAYLOAD" + + else + echo "No inline comments, posting summary only..." + + COMMENT_BODY="## 번역 리뷰 (GitHub Models) + + ### 요약 + $SUMMARY + + ### 잘된 점 + ${GOOD_POINTS:-"특별히 언급할 사항 없음"} + + ### 수정 제안 + 수정 제안 없음 + + --- + GPT-4o via GitHub Models | tokens: ${INPUT_TOKENS} in / ${OUTPUT_TOKENS} out" + + gh pr comment "$PR_NUMBER" --body "$COMMENT_BODY" + fi + + echo "Review posted successfully." diff --git a/.github/workflows/trnaslate-review.yml b/.github/workflows/trnaslate-review.yml deleted file mode 100644 index be05d0eba..000000000 --- a/.github/workflows/trnaslate-review.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Translation Review (GitHub Models) - -on: - pull_request: - types: [opened, synchronize] - paths: - - "src/content/**/*.mdx" - -permissions: - contents: read - pull-requests: write - models: read - -jobs: - review: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get changed MDX files - id: changed - run: | - FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep '\.mdx$' || true) - if [ -z "$FILES" ]; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "No MDX files changed." - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Save PR diff to file - if: steps.changed.outputs.skip == 'false' - run: | - gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-diff.txt - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Review with GitHub Models - if: steps.changed.outputs.skip == 'false' - uses: actions/github-script@v7 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - script: | - const fs = require("fs"); - - const diff = fs.readFileSync("/tmp/pr-diff.txt", "utf8"); - const agents = fs.readFileSync("AGENTS.md", "utf8"); - const reviewInstructions = fs.readFileSync( - ".github/copilot-review-instructions.md", - "utf8" - ); - - const systemPrompt = [ - "당신은 React Hook Form 한국어 번역 프로젝트의 코드 리뷰어입니다.", - "반드시 한국어로 응답하세요.", - "", - "## 번역 규칙", - agents, - "", - "## 리뷰 지침", - reviewInstructions, - "", - "## 리뷰 형식", - "", - "PR diff를 분석하고 번역 품질을 리뷰하세요.", - "다음 항목을 중심으로 검토합니다:", - "", - "1. **번역 용어 규칙 준수**: AGENTS.md의 번역 용어 표를 따르는지", - "2. **자연스러운 한국어 표현**: 어색한 번역투가 없는지", - "3. **기술적 정확성**: 원문의 의미가 정확히 전달되는지", - "4. **문서 구조**: MDX frontmatter, 섹션 제목 규칙을 따르는지", - "5. **코드 블록**: 코드 예제가 변경되지 않았는지 (주석 번역은 허용)", - "", - "리뷰 결과를 다음 형식으로 작성하세요:", - "", - "### 요약", - "전체적인 번역 품질에 대한 간단한 평가 (1-2문장)", - "", - "### 수정 제안", - "수정이 필요한 부분이 있다면 각각:", - "- **파일**: 파일 경로", - "- **위치**: 해당 내용", - "- **현재**: 현재 번역", - "- **제안**: 수정 제안", - "- **이유**: 수정 이유", - "", - '수정할 부분이 없으면 "수정 제안 없음"이라고 작성하세요.', - "", - "### 잘된 점", - "번역이 잘 된 부분이 있다면 간단히 언급하세요.", - ].join("\n"); - - const userMessage = "다음 PR diff를 리뷰해주세요:\n\n```diff\n" + diff + "\n```"; - - const response = await fetch("https://models.github.ai/inference/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`, - }, - body: JSON.stringify({ - model: "openai/gpt-4o", - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content: userMessage }, - ], - max_tokens: 4096, - }), - }); - - if (!response.ok) { - const error = await response.text(); - core.setFailed(`GitHub Models API error: ${response.status} ${error}`); - return; - } - - const data = await response.json(); - const reviewText = data.choices?.[0]?.message?.content; - - if (!reviewText) { - core.setFailed("No review text returned from GitHub Models."); - return; - } - - const usage = data.usage || {}; - const inputTokens = usage.prompt_tokens || 0; - const outputTokens = usage.completion_tokens || 0; - core.info(`Tokens used - input: ${inputTokens}, output: ${outputTokens}`); - - // Post review as PR comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body: [ - "## 번역 리뷰 (GitHub Models)", - "", - reviewText, - "", - "---", - `GPT-4o via GitHub Models | tokens: ${inputTokens} in / ${outputTokens} out`, - ].join("\n"), - }); - - core.info("Review comment posted successfully."); diff --git a/src/content/docs/useform/reset.mdx b/src/content/docs/useform/reset.mdx index bec0c18f3..b43fe316b 100644 --- a/src/content/docs/useform/reset.mdx +++ b/src/content/docs/useform/reset.mdx @@ -6,7 +6,7 @@ sidebar: apiLinks ## \ `reset:` `(values?: T | ResetAction, options?: Record) => void` -전체 폼 상태, 필드 참조 및 구독을 초기화합니다. 선택적 인자가 있으며, 부분적인 폼 상태 초기화를 허용할 수 있습니다. +전체 폼 상태, 필드 참조 및 구독을 초기화합니다. 선택적 인자를 통해 부분적인 폼 상태만 초기화할 수도 있습니다. ### Props diff --git a/src/content/docs/useform/seterror.mdx b/src/content/docs/useform/seterror.mdx index a06e1de9a..98271854a 100644 --- a/src/content/docs/useform/seterror.mdx +++ b/src/content/docs/useform/seterror.mdx @@ -6,7 +6,7 @@ sidebar: apiLinks ## \ `setError:` `(name: string, error: FieldError, { shouldFocus?: boolean }) => void` -이 함수는 하나 이상의 에러를 수동으로 설정할 수 있도록 합니다. +이 함수를 사용하면 하나 이상의 에러를 직접 설정할 수 있습니다. ### Props diff --git a/src/content/docs/useform/trigger.mdx b/src/content/docs/useform/trigger.mdx index 5198f4fd7..4384f02b8 100644 --- a/src/content/docs/useform/trigger.mdx +++ b/src/content/docs/useform/trigger.mdx @@ -6,7 +6,7 @@ sidebar: apiLinks ## `trigger:` `(name?: string | string[]) => Promise` -폼 또는 인풋의 유효성 검사를 수동으로 트리거합니다. 이 메서드는 의존적인 유효성 검사가 있는 경우(입력 유효성 검사가 다른 입력 값에 의존하는 경우)에도 유용합니다. +폼 또는 인풋의 유효성 검사를 직접 트리거합니다. 이 메서드는 의존적인 유효성 검사가 있을 때(한 입력의 유효성 검사가 다른 입력 값에 의존하는 경우) 유용합니다. ### Props