From ccc661aaeddded060a53709b5b251aa1d14e833a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 20 Jan 2026 05:23:06 +0000 Subject: [PATCH 1/8] =?UTF-8?q?MapSy-FE=20=EB=B2=84=EC=A0=84=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20:=20docs=20:=20v1.0.16=20README=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aded48..1b6e778 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,6 @@ samples, guidance on mobile development, and a full API reference. --- -## 최신 버전 : v1.0.14 (2026-01-19) +## 최신 버전 : v1.0.16 (2026-01-20) [전체 버전 기록 보기](CHANGELOG.md) From ec37820feff72b316012145cbed8961722f67a7e Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Tue, 20 Jan 2026 15:36:53 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20Spring=20init=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20workflow=20=EA=B5=AC=ED=98=84=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=20:=20feat=20:=20Projects=20Sync=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=B6=94=EA=B0=80=20https://github.com/Ma?= =?UTF-8?q?pSee-Lab/MapSee-BE/issues/2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml | 836 ++++++++++++++++++ 1 file changed, 836 insertions(+) create mode 100644 .github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml diff --git a/.github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml b/.github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml new file mode 100644 index 0000000..e03b326 --- /dev/null +++ b/.github/workflows/PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml @@ -0,0 +1,836 @@ +# =================================================================== +# GitHub Projects 자동 관리 워크플로우 +# =================================================================== +# +# 이 워크플로우는 GitHub Issue와 Projects를 자동으로 동기화합니다. +# +# 작동 방식: +# 1. Issue 생성 시 자동으로 프로젝트에 추가 (PR은 옵션) +# 2. Issue Label 변경 시 Projects Status 자동 동기화 +# 3. Issue 닫기 시 Projects Status를 "작업 완료"로 자동 변경 (PR은 옵션) +# +# 지원 기능: +# - Issue 생성 → 프로젝트 자동 추가 (기본 Status: "작업 전") +# - PR 생성 → 프로젝트 자동 추가 (옵션, 기본값: 비활성화) +# - Label 변경 → Projects Status 실시간 동기화 (Issue만 지원) +# • 작업 전 → 작업 전 +# • 작업 중 → 작업 중 +# • 확인 대기 → 확인 대기 +# • 피드백 → 피드백 +# • 작업 완료 → 작업 완료 +# • 취소 → 취소 +# - Issue 닫기 → "작업 완료" Status 자동 설정 +# - PR 닫기 → "작업 완료" Status 자동 설정 (옵션, 기본값: 비활성화) +# - 여러 Status Label 동시 존재 시 우선순위 정책 적용 +# +# Label 우선순위: +# 작업 완료 > 취소 > 피드백 > 확인 대기 > 작업 중 > 작업 전 +# +# 필수 설정: +# - Organization Secret: _GITHUB_PAT_TOKEN (모든 작업에 사용) +# • 필요 권한: repo (전체), project (read:project, write:project) +# • Classic PAT 필요 (Fine-grained token은 GraphQL API 미지원) +# +# 환경변수 설정: +# - PROJECT_URL: GitHub Projects URL (필수) +# - STATUS_FIELD: Projects의 Status 필드명 (기본값: "Status") +# - ENABLE_PR_AUTO_ADD: PR 생성 시 프로젝트 자동 추가 (기본값: false) +# - ENABLE_PR_AUTO_CLOSE: PR 닫기 시 작업 완료 처리 (기본값: false) +# - STATUS_PRIORITY: Status Label 우선순위 (JSON 배열, 커스터마이징 가능) +# - DONE_STATUS: Issue/PR 닫기 시 설정할 Status (기본값: "작업 완료") +# - DEFAULT_STATUS: Issue 생성 시 기본 Status (기본값: "작업 전", 선택적) +# +# 사용 예시: +# - Issue만 자동화: 기본 설정 사용 (변경 불필요) +# - PR도 자동화: ENABLE_PR_AUTO_ADD와 ENABLE_PR_AUTO_CLOSE를 true로 변경 +# - Status 우선순위 변경: STATUS_PRIORITY JSON 배열 수정 +# +# =================================================================== + +name: PROJECT-COMMON-PROJECTS-SYNC-MANAGER + +on: + issues: + types: [opened, labeled, unlabeled, closed] + pull_request: + types: [opened, closed] + +# =================================================================== +# 설정 변수 +# =================================================================== +env: + PROJECT_URL: https://github.com/orgs/MapSee-Lab/projects/1 + STATUS_FIELD: Status + + # PR 자동화 옵션 (기본값: false - Issue만 처리) + ENABLE_PR_AUTO_ADD: false # PR 생성 시 프로젝트 자동 추가 + ENABLE_PR_AUTO_CLOSE: false # PR 닫기 시 작업 완료 처리 + + # Status 관리 옵션 + STATUS_PRIORITY: '["작업 완료","취소","피드백","확인 대기","작업 중","작업 전"]' # Status Label 우선순위 (JSON 배열) + DONE_STATUS: "작업 완료" # Issue/PR 닫기 시 설정할 Status + DEFAULT_STATUS: "작업 전" # Issue 생성 시 기본 Status (선택적, actions/add-to-project가 자동 설정) + +permissions: + issues: write + pull-requests: write + contents: read + +jobs: + # =================================================================== + # Job 1: Issue/PR 생성 시 프로젝트에 자동 추가 + # =================================================================== + add-to-project: + name: 프로젝트에 Issue/PR 추가 + if: | + github.event.action == 'opened' && + ( + github.event_name == 'issues' || + (github.event_name == 'pull_request' && false) + ) + runs-on: ubuntu-latest + steps: + - name: 프로젝트에 Issue/PR 추가 + uses: actions/add-to-project@v0.5.0 + with: + project-url: ${{ env.PROJECT_URL }} + github-token: ${{ secrets._GITHUB_PAT_TOKEN }} + + - name: 추가 완료 로그 + run: | + if [ "${{ github.event_name }}" == "issues" ]; then + echo "✅ Issue가 프로젝트에 추가되었습니다." + echo " • 번호: #${{ github.event.issue.number }}" + else + echo "✅ PR이 프로젝트에 추가되었습니다. (ENABLE_PR_AUTO_ADD: true)" + echo " • 번호: #${{ github.event.pull_request.number }}" + fi + echo " • 프로젝트: ${{ env.PROJECT_URL }}" + + # =================================================================== + # Job 2: Label 변경 시 Projects Status 동기화 + # =================================================================== + sync-label-to-status: + name: Label을 Projects Status로 동기화 + if: | + github.event_name == 'issues' && + (github.event.action == 'labeled' || github.event.action == 'unlabeled') + runs-on: ubuntu-latest + steps: + - name: 현재 Issue의 모든 Label 조회 + id: get-labels + uses: actions/github-script@v7 + with: + github-token: ${{ secrets._GITHUB_PAT_TOKEN }} + script: | + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📋 Label 조회 시작'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const labels = issue.data.labels.map(label => label.name); + console.log(`📌 Issue #${context.issue.number}의 현재 Labels:`); + console.log(` ${labels.join(', ') || '(없음)'}`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + return labels; + + - name: Status로 매핑할 Label 결정 + id: determine-status + uses: actions/github-script@v7 + env: + STATUS_PRIORITY: ${{ env.STATUS_PRIORITY }} + with: + github-token: ${{ secrets._GITHUB_PAT_TOKEN }} + script: | + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🎯 Status 결정 시작'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + // ===== 1. Labels 배열 검증 및 정제 ===== + let labels = ${{ steps.get-labels.outputs.result }}; + + if (!Array.isArray(labels)) { + console.error('❌ labels가 배열이 아닙니다. 빈 배열로 대체합니다.'); + console.error(` 실제 타입: ${typeof labels}`); + console.error(` 실제 값: ${JSON.stringify(labels)}`); + labels = []; + } + + // 유효하지 않은 Label 필터링 (null, undefined, 빈 문자열, 비문자열 제거) + const originalLabelCount = labels.length; + labels = labels.filter(label => { + if (typeof label !== 'string') { + console.warn(`⚠️ 비문자열 Label 제거됨 (타입: ${typeof label}, 값: ${JSON.stringify(label)})`); + return false; + } + if (label.trim() === '') { + console.warn('⚠️ 빈 문자열 Label 제거됨'); + return false; + } + return true; + }); + + if (labels.length < originalLabelCount) { + console.log(`🧹 유효하지 않은 Label ${originalLabelCount - labels.length}개 제거됨`); + } + + console.log(`📌 유효한 Label 개수: ${labels.length}`); + + // ===== 2. Status 우선순위 배열 검증 및 정제 ===== + let statusPriority; + try { + const statusPriorityEnv = process.env.STATUS_PRIORITY || '["작업 완료","취소","피드백","확인 대기","작업 중","작업 전"]'; + statusPriority = JSON.parse(statusPriorityEnv); + + // 배열 타입 검증 + if (!Array.isArray(statusPriority)) { + throw new Error(`STATUS_PRIORITY must be an array (actual type: ${typeof statusPriority})`); + } + + // 빈 배열 검증 + if (statusPriority.length === 0) { + throw new Error('STATUS_PRIORITY must be a non-empty array'); + } + + // 각 요소가 유효한 문자열인지 검증 및 필터링 + const originalPriorityCount = statusPriority.length; + statusPriority = statusPriority.filter(item => { + if (typeof item !== 'string') { + console.warn(`⚠️ STATUS_PRIORITY에서 비문자열 요소 제거됨 (타입: ${typeof item}, 값: ${JSON.stringify(item)})`); + return false; + } + if (item.trim() === '') { + console.warn('⚠️ STATUS_PRIORITY에서 빈 문자열 요소 제거됨'); + return false; + } + return true; + }); + + // 필터링 후 빈 배열인지 재검증 + if (statusPriority.length === 0) { + throw new Error('STATUS_PRIORITY must contain at least one non-empty string'); + } + + // 유효하지 않은 요소 제거 알림 + if (statusPriority.length < originalPriorityCount) { + console.warn(`⚠️ STATUS_PRIORITY에서 유효하지 않은 값 ${originalPriorityCount - statusPriority.length}개 제거됨`); + } + + console.log(`📊 설정된 Status 우선순위: ${statusPriority.join(' > ')}`); + + } catch (error) { + console.error(`❌ STATUS_PRIORITY 파싱/검증 실패, 기본값 사용: ${error.message}`); + statusPriority = ['작업 완료','취소','피드백','확인 대기','작업 중','작업 전']; + console.log(`📊 기본 Status 우선순위: ${statusPriority.join(' > ')}`); + } + + // ===== 3. 현재 Label 중 Status Label 찾기 (모든 매칭 추적) ===== + const foundStatusLabels = []; + let targetStatus = ''; + + for (const status of statusPriority) { + if (labels.includes(status)) { + foundStatusLabels.push(status); + // 첫 번째로 발견된 것만 targetStatus로 설정 + if (!targetStatus) { + targetStatus = status; + } + } + } + + // ===== 4. 결과 로깅 (여러 Status Label이 있을 경우 명시적 알림) ===== + if (foundStatusLabels.length === 0) { + console.log('⚠️ Status Label이 없습니다. Status 업데이트 건너뜀'); + } else if (foundStatusLabels.length === 1) { + console.log(`✅ Status Label 발견: "${targetStatus}"`); + console.log(`🎯 Projects Status로 설정할 값: "${targetStatus}"`); + } else { + // 여러 Status Label이 동시에 존재하는 경우 + console.log(`⚠️ 여러 개의 Status Label이 발견되었습니다:`); + console.log(` 발견된 Labels: ${foundStatusLabels.join(', ')}`); + console.log(` 우선순위에 따라 선택된 Label: "${targetStatus}"`); + console.log(` 무시된 Labels: ${foundStatusLabels.slice(1).join(', ')}`); + console.log(`🎯 Projects Status로 설정할 값: "${targetStatus}"`); + console.log(''); + console.log('💡 권장사항: 하나의 Issue에는 하나의 Status Label만 사용하세요.'); + } + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + return targetStatus; + + - name: Projects Status 업데이트 + if: steps.determine-status.outputs.result != '' + uses: actions/github-script@v7 + env: + TARGET_STATUS: ${{ fromJSON(steps.determine-status.outputs.result) }} + with: + github-token: ${{ secrets._GITHUB_PAT_TOKEN }} + script: | + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🔄 Projects Status 업데이트 시작'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + const targetStatus = process.env.TARGET_STATUS; + const issueNodeId = context.payload.issue.node_id; + const projectUrl = '${{ env.PROJECT_URL }}'; + + console.log(`📌 Issue Node ID: ${issueNodeId}`); + console.log(`📌 목표 Status: "${targetStatus}"`); + console.log(`📌 프로젝트 URL: ${projectUrl}`); + + try { + // 1. 프로젝트 번호 추출 + const projectMatch = projectUrl.match(/\/projects\/(\d+)/); + if (!projectMatch) { + throw new Error('프로젝트 URL에서 번호를 추출할 수 없습니다.'); + } + const projectNumber = parseInt(projectMatch[1]); + console.log(`📊 프로젝트 번호: ${projectNumber}`); + + // 2. 조직 프로젝트 정보 조회 + const orgLogin = context.repo.owner; + console.log(`🏢 조직명: ${orgLogin}`); + + const projectQuery = ` + query($orgLogin: String!, $projectNumber: Int!) { + organization(login: $orgLogin) { + projectV2(number: $projectNumber) { + id + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + `; + + const projectData = await github.graphql(projectQuery, { + orgLogin, + projectNumber + }); + + const project = projectData.organization.projectV2; + const projectId = project.id; + console.log(`✅ 프로젝트 ID: ${projectId}`); + + // 3. Status 필드 및 옵션 ID 찾기 + const statusField = project.fields.nodes.find( + field => field.name === '${{ env.STATUS_FIELD }}' + ); + + if (!statusField) { + throw new Error('Status 필드를 찾을 수 없습니다.'); + } + + const fieldId = statusField.id; + console.log(`✅ Status 필드 ID: ${fieldId}`); + + const statusOption = statusField.options.find( + option => option.name === targetStatus + ); + + if (!statusOption) { + throw new Error(`"${targetStatus}" 옵션을 찾을 수 없습니다.`); + } + + const optionId = statusOption.id; + console.log(`✅ "${targetStatus}" 옵션 ID: ${optionId}`); + + // 4. Issue의 프로젝트 아이템 ID 조회 + const itemQuery = ` + query($issueId: ID!) { + node(id: $issueId) { + ... on Issue { + projectItems(first: 10) { + nodes { + id + project { + id + } + } + } + } + } + } + `; + + const itemData = await github.graphql(itemQuery, { + issueId: issueNodeId + }); + + const projectItem = itemData.node.projectItems.nodes.find( + item => item.project.id === projectId + ); + + if (!projectItem) { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('⚠️ 이 Issue가 프로젝트에 추가되지 않았습니다.'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(''); + console.log('📋 현재 상태:'); + console.log(` • Issue 번호: #${context.issue.number}`); + console.log(` • 프로젝트: ${projectUrl}`); + console.log(` • 프로젝트에 추가됨: ❌ 아니오`); + console.log(''); + console.log('💡 가능한 원인:'); + console.log(' 1. Issue가 생성된 직후 아직 프로젝트에 추가되지 않음'); + console.log(' 2. add-to-project Job이 실패했거나 실행되지 않음'); + console.log(' 3. Issue가 수동으로 프로젝트에서 제거됨'); + console.log(' 4. 프로젝트 URL이 잘못 설정됨'); + console.log(''); + console.log('🔧 해결 방법:'); + console.log(' 1. 프로젝트 페이지에서 수동으로 Issue를 추가하세요.'); + console.log(' 2. Issue Label을 다시 변경하여 워크플로우를 재실행하세요.'); + console.log(' 3. 워크플로우 로그에서 add-to-project Job 실행 여부를 확인하세요.'); + console.log(' 4. 환경변수 PROJECT_URL이 올바른지 확인하세요.'); + console.log(''); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + return; + } + + const itemId = projectItem.id; + console.log(`✅ 프로젝트 아이템 ID: ${itemId}`); + + // 5. Status 업데이트 뮤테이션 실행 + const updateMutation = ` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue( + input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { singleSelectOptionId: $optionId } + } + ) { + projectV2Item { + id + } + } + } + `; + + await github.graphql(updateMutation, { + projectId, + itemId, + fieldId, + optionId + }); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🎉 Projects Status 업데이트 완료!'); + console.log(` • Issue: #${context.issue.number}`); + console.log(` • Status: "${targetStatus}"`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + } catch (error) { + console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.error('❌ Status 업데이트 실패'); + console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + // 에러 타입 분석 및 상세 안내 + const errorMessage = error.message || JSON.stringify(error); + const errorStatus = error.status || (error.response && error.response.status); + + if (errorStatus === 401 || errorMessage.includes('401') || errorMessage.includes('Unauthorized')) { + // 인증 오류 + console.error('🔐 인증 오류 (401 Unauthorized)'); + console.error(''); + console.error('원인:'); + console.error(' 1. _GITHUB_PAT_TOKEN Secret이 설정되지 않았습니다.'); + console.error(' 2. PAT 토큰이 만료되었습니다.'); + console.error(' 3. PAT 토큰에 필요한 권한(repo, project)이 없습니다.'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. GitHub Settings > Developer settings > Personal access tokens'); + console.error(' 2. Classic PAT 생성 (Fine-grained token은 GraphQL 미지원)'); + console.error(' 3. 권한 부여: repo (전체), project (read:project, write:project)'); + console.error(' 4. Organization Secrets에 _GITHUB_PAT_TOKEN으로 등록'); + + } else if (errorStatus === 404 || errorMessage.includes('404') || errorMessage.includes('Not Found')) { + // 리소스 찾을 수 없음 + console.error('🔍 리소스를 찾을 수 없습니다 (404 Not Found)'); + console.error(''); + console.error('원인:'); + console.error(' 1. 프로젝트 URL이 잘못되었습니다.'); + console.error(' 2. STATUS_FIELD 이름이 프로젝트 필드명과 일치하지 않습니다.'); + console.error(' 3. 설정하려는 Status 값이 프로젝트에 존재하지 않습니다.'); + console.error(' 4. PAT 토큰에 해당 프로젝트 접근 권한이 없습니다.'); + console.error(''); + console.error('해결 방법:'); + console.error(` 1. 프로젝트 URL 확인: ${projectUrl}`); + console.error(` 2. Status 필드명 확인: "${{ env.STATUS_FIELD }}"`); + console.error(` 3. Status 옵션 확인: "${targetStatus}"`); + console.error(' 4. PAT 토큰이 해당 Organization에 접근 가능한지 확인'); + + } else if (errorMessage.includes('rate limit') || errorMessage.includes('abuse') || errorMessage.includes('429')) { + // Rate Limiting + console.error('⏱️ API Rate Limit 초과 (429 Too Many Requests)'); + console.error(''); + console.error('원인:'); + console.error(' GitHub API 호출 횟수 제한을 초과했습니다.'); + console.error(' (시간당 5000 요청 제한)'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. 잠시 후 다시 시도하세요.'); + console.error(' 2. 워크플로우 트리거 빈도를 줄이세요.'); + console.error(' 3. GitHub API Rate Limit 상태를 확인하세요.'); + + } else if (errorMessage.includes('ETIMEDOUT') || errorMessage.includes('ECONNREFUSED') || errorMessage.includes('network')) { + // 네트워크 오류 + console.error('🌐 네트워크 오류'); + console.error(''); + console.error('원인:'); + console.error(' GitHub API 서버와 통신할 수 없습니다.'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. GitHub 서비스 상태를 확인하세요: https://www.githubstatus.com'); + console.error(' 2. 잠시 후 다시 시도하세요.'); + console.error(' 3. 워크플로우가 자동으로 재시도됩니다.'); + + } else if (errorMessage.includes('Cannot be updated') || errorMessage.includes('Field cannot be updated')) { + // 필드 업데이트 불가 + console.error('🚫 필드 업데이트 불가'); + console.error(''); + console.error('원인:'); + console.error(' 해당 필드는 GraphQL API로 업데이트할 수 없습니다.'); + console.error(' (Labels, Assignees, Milestone 등은 뮤테이션 미지원)'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. STATUS_FIELD가 Single Select 타입인지 확인하세요.'); + console.error(' 2. 프로젝트 설정에서 필드 타입을 확인하세요.'); + + } else { + // 기타 오류 + console.error('❓ 알 수 없는 오류'); + console.error(''); + console.error('상세 정보:'); + } + + console.error(''); + console.error('전체 에러 메시지:'); + console.error(errorMessage); + if (error.stack) { + console.error(''); + console.error('Stack Trace:'); + console.error(error.stack); + } + console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + throw error; + } + + # =================================================================== + # Job 3: Issue/PR 닫기 시 "작업 완료" Status로 변경 + # =================================================================== + move-closed-to-done: + name: 닫힌 Issue/PR을 작업 완료로 이동 + if: | + github.event.action == 'closed' && + ( + github.event_name == 'issues' || + (github.event_name == 'pull_request' && false) + ) + runs-on: ubuntu-latest + steps: + - name: Projects Status를 "작업 완료"로 업데이트 + uses: actions/github-script@v7 + env: + DONE_STATUS: ${{ env.DONE_STATUS }} + with: + github-token: ${{ secrets._GITHUB_PAT_TOKEN }} + script: | + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🏁 Issue/PR 닫기 감지 - 작업 완료 처리'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + const targetStatus = process.env.DONE_STATUS || '작업 완료'; + const itemNodeId = context.payload.issue?.node_id || context.payload.pull_request?.node_id; + const itemNumber = context.payload.issue?.number || context.payload.pull_request?.number; + const itemType = context.payload.issue ? 'Issue' : 'Pull Request'; + const projectUrl = '${{ env.PROJECT_URL }}'; + + console.log(`📌 ${itemType} #${itemNumber}`); + console.log(`📌 Node ID: ${itemNodeId}`); + console.log(`📌 목표 Status: "${targetStatus}"`); + + try { + // 1. 프로젝트 번호 추출 + const projectMatch = projectUrl.match(/\/projects\/(\d+)/); + if (!projectMatch) { + throw new Error('프로젝트 URL에서 번호를 추출할 수 없습니다.'); + } + const projectNumber = parseInt(projectMatch[1]); + + // 2. 조직 프로젝트 정보 조회 + const orgLogin = context.repo.owner; + + const projectQuery = ` + query($orgLogin: String!, $projectNumber: Int!) { + organization(login: $orgLogin) { + projectV2(number: $projectNumber) { + id + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + `; + + const projectData = await github.graphql(projectQuery, { + orgLogin, + projectNumber + }); + + const project = projectData.organization.projectV2; + const projectId = project.id; + + // 3. Status 필드 및 "작업 완료" 옵션 ID 찾기 + const statusField = project.fields.nodes.find( + field => field.name === '${{ env.STATUS_FIELD }}' + ); + + if (!statusField) { + throw new Error('Status 필드를 찾을 수 없습니다.'); + } + + const fieldId = statusField.id; + + const doneOption = statusField.options.find( + option => option.name === targetStatus + ); + + if (!doneOption) { + throw new Error(`"${targetStatus}" 옵션을 찾을 수 없습니다.`); + } + + const optionId = doneOption.id; + console.log(`✅ "${targetStatus}" 옵션 ID: ${optionId}`); + + // 4. Issue/PR의 프로젝트 아이템 ID 조회 + const itemQuery = context.payload.issue + ? ` + query($itemId: ID!) { + node(id: $itemId) { + ... on Issue { + projectItems(first: 10) { + nodes { + id + project { + id + } + } + } + } + } + } + ` + : ` + query($itemId: ID!) { + node(id: $itemId) { + ... on PullRequest { + projectItems(first: 10) { + nodes { + id + project { + id + } + } + } + } + } + } + `; + + const itemData = await github.graphql(itemQuery, { + itemId: itemNodeId + }); + + const projectItem = itemData.node.projectItems.nodes.find( + item => item.project.id === projectId + ); + + if (!projectItem) { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(`⚠️ 이 ${itemType}가 프로젝트에 추가되지 않았습니다.`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(''); + console.log('📋 현재 상태:'); + console.log(` • ${itemType} 번호: #${itemNumber}`); + console.log(` • 프로젝트: ${projectUrl}`); + console.log(` • 프로젝트에 추가됨: ❌ 아니오`); + console.log(''); + console.log('💡 가능한 원인:'); + console.log(' 1. 이 Issue/PR이 프로젝트에 추가되지 않았습니다.'); + console.log(' 2. add-to-project Job이 실패했거나 실행되지 않음'); + console.log(' 3. 수동으로 프로젝트에서 제거됨'); + console.log(' 4. 프로젝트 URL이 잘못 설정됨'); + console.log(''); + console.log('🔧 해결 방법:'); + console.log(' 1. 프로젝트 페이지에서 수동으로 추가 후 Status를 "작업 완료"로 변경하세요.'); + console.log(' 2. 워크플로우 로그에서 add-to-project Job 실행 여부를 확인하세요.'); + console.log(' 3. 환경변수 PROJECT_URL이 올바른지 확인하세요.'); + console.log(''); + console.log('ℹ️ 자동 완료 처리를 건너뜁니다.'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + return; + } + + const itemId = projectItem.id; + console.log(`✅ 프로젝트 아이템 ID: ${itemId}`); + + // 5. Status 업데이트 뮤테이션 실행 + const updateMutation = ` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue( + input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { singleSelectOptionId: $optionId } + } + ) { + projectV2Item { + id + } + } + } + `; + + await github.graphql(updateMutation, { + projectId, + itemId, + fieldId, + optionId + }); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🎉 작업 완료 처리 성공!'); + console.log(` • ${itemType}: #${itemNumber}`); + console.log(` • Status: "${targetStatus}"`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + } catch (error) { + console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.error('❌ 작업 완료 처리 실패'); + console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + // 에러 타입 분석 및 상세 안내 + const errorMessage = error.message || JSON.stringify(error); + const errorStatus = error.status || (error.response && error.response.status); + + if (errorStatus === 401 || errorMessage.includes('401') || errorMessage.includes('Unauthorized')) { + // 인증 오류 + console.error('🔐 인증 오류 (401 Unauthorized)'); + console.error(''); + console.error('원인:'); + console.error(' 1. _GITHUB_PAT_TOKEN Secret이 설정되지 않았습니다.'); + console.error(' 2. PAT 토큰이 만료되었습니다.'); + console.error(' 3. PAT 토큰에 필요한 권한(repo, project)이 없습니다.'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. GitHub Settings > Developer settings > Personal access tokens'); + console.error(' 2. Classic PAT 생성 (Fine-grained token은 GraphQL 미지원)'); + console.error(' 3. 권한 부여: repo (전체), project (read:project, write:project)'); + console.error(' 4. Organization Secrets에 _GITHUB_PAT_TOKEN으로 등록'); + + } else if (errorStatus === 404 || errorMessage.includes('404') || errorMessage.includes('Not Found')) { + // 리소스 찾을 수 없음 + console.error('🔍 리소스를 찾을 수 없습니다 (404 Not Found)'); + console.error(''); + console.error('원인:'); + console.error(' 1. 프로젝트 URL이 잘못되었습니다.'); + console.error(' 2. STATUS_FIELD 이름이 프로젝트 필드명과 일치하지 않습니다.'); + console.error(' 3. DONE_STATUS 값이 프로젝트에 존재하지 않습니다.'); + console.error(' 4. PAT 토큰에 해당 프로젝트 접근 권한이 없습니다.'); + console.error(''); + console.error('해결 방법:'); + console.error(` 1. 프로젝트 URL 확인: ${projectUrl}`); + console.error(` 2. Status 필드명 확인: "${{ env.STATUS_FIELD }}"`); + console.error(` 3. DONE_STATUS 확인: "${targetStatus}"`); + console.error(' 4. PAT 토큰이 해당 Organization에 접근 가능한지 확인'); + + } else if (errorMessage.includes('rate limit') || errorMessage.includes('abuse') || errorMessage.includes('429')) { + // Rate Limiting + console.error('⏱️ API Rate Limit 초과 (429 Too Many Requests)'); + console.error(''); + console.error('원인:'); + console.error(' GitHub API 호출 횟수 제한을 초과했습니다.'); + console.error(' (시간당 5000 요청 제한)'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. 잠시 후 다시 시도하세요.'); + console.error(' 2. 워크플로우 트리거 빈도를 줄이세요.'); + console.error(' 3. GitHub API Rate Limit 상태를 확인하세요.'); + + } else if (errorMessage.includes('ETIMEDOUT') || errorMessage.includes('ECONNREFUSED') || errorMessage.includes('network')) { + // 네트워크 오류 + console.error('🌐 네트워크 오류'); + console.error(''); + console.error('원인:'); + console.error(' GitHub API 서버와 통신할 수 없습니다.'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. GitHub 서비스 상태를 확인하세요: https://www.githubstatus.com'); + console.error(' 2. 잠시 후 다시 시도하세요.'); + console.error(' 3. 워크플로우가 자동으로 재시도됩니다.'); + + } else if (errorMessage.includes('Cannot be updated') || errorMessage.includes('Field cannot be updated')) { + // 필드 업데이트 불가 + console.error('🚫 필드 업데이트 불가'); + console.error(''); + console.error('원인:'); + console.error(' 해당 필드는 GraphQL API로 업데이트할 수 없습니다.'); + console.error(' (Labels, Assignees, Milestone 등은 뮤테이션 미지원)'); + console.error(''); + console.error('해결 방법:'); + console.error(' 1. STATUS_FIELD가 Single Select 타입인지 확인하세요.'); + console.error(' 2. 프로젝트 설정에서 필드 타입을 확인하세요.'); + + } else { + // 기타 오류 + console.error('❓ 알 수 없는 오류'); + console.error(''); + console.error('상세 정보:'); + } + + console.error(''); + console.error('전체 에러 메시지:'); + console.error(errorMessage); + if (error.stack) { + console.error(''); + console.error('Stack Trace:'); + console.error(error.stack); + } + console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + // 실패해도 워크플로우는 성공으로 처리 (GitHub 기본 자동화가 있을 수 있음) + console.log(''); + console.log('⚠️ GitHub Projects 기본 자동화가 활성화되어 있다면 자동으로 처리됩니다.'); + console.log(' Projects > Workflows 메뉴에서 "Item closed" 자동화를 확인하세요.'); + } From 0db83fcd8bf8bc98dc757eb87a4082884bbe64ba Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 20 Jan 2026 06:37:03 +0000 Subject: [PATCH 3/8] =?UTF-8?q?MapSy-FE=20=EB=B2=84=EC=A0=84=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=201.0.17=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- version.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index f8f1210..30659da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mapsy description: "MapSy - Flutter Application" publish_to: "none" -version: 1.0.16+16 +version: 1.0.17+17 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index 70e5bc0..fc45edc 100644 --- a/version.yml +++ b/version.yml @@ -34,12 +34,12 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.0.16" -version_code: 17 # app build number +version: "1.0.17" +version_code: 18 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-01-20 05:18:32" - last_updated_by: "EM-H20" + last_updated: "2026-01-20 06:37:03" + last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE" integration_date: "2026-01-19" From 7ff409e9c79cdf175304f31140ea517eb9e4b55a Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Tue, 27 Jan 2026 15:35:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20Spring=20init=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20workflow=20=EA=B5=AC=ED=98=84=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=20:=20feat=20:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20custom?= =?UTF-8?q?=20command=20=EC=B6=94=EA=B0=80=20https://github.com/MapSee-Lab?= =?UTF-8?q?/MapSee-BE/issues/2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/init-workflow.md | 166 +++++++ .claude/commands/report.md | 46 ++ .claude/scripts/README.md | 54 +++ .cursor/commands/init-workflow.md | 166 +++++++ .cursor/commands/report.md | 46 ++ .cursor/scripts/README.md | 51 +++ .cursor/scripts/worktree_manager.py | 644 ++++++++++++++++++++++++++++ 7 files changed, 1173 insertions(+) create mode 100644 .claude/commands/init-workflow.md create mode 100644 .claude/scripts/README.md create mode 100644 .cursor/commands/init-workflow.md create mode 100644 .cursor/scripts/worktree_manager.py diff --git a/.claude/commands/init-workflow.md b/.claude/commands/init-workflow.md new file mode 100644 index 0000000..6ce2d84 --- /dev/null +++ b/.claude/commands/init-workflow.md @@ -0,0 +1,166 @@ +# init-workflow + +Git worktree를 자동으로 생성하는 커맨드입니다. + +브랜치명을 입력받아 자동으로: +1. 브랜치명에서 `#` 문자 제거 (Git 브랜치명으로 사용) +2. 브랜치가 없으면 생성 (현재 브랜치에서 분기) +3. 브랜치명의 특수문자를 `_`로 변환하여 폴더명 생성 +4. `{프로젝트명}-Worktree` 폴더에 worktree 생성 (예: `RomRom-FE-Worktree`) +5. 설정 파일 자동 복사 (Firebase, iOS, Android 키 등) +6. 이미 존재하면 경로만 출력 + +## 사용법 + +``` +/init-workflow + +20260120_#163_Github_Projects_에_대한_템플릿_개발_필요_및_관련_Sync_워크플로우_개발_필요 +``` + +## 실행 로직 + +1. 사용자 입력에서 두 번째 줄의 브랜치명 추출 +2. 브랜치명에서 `#` 문자 제거 +3. 임시 Python 스크립트 파일 생성 (인코딩 문제 해결) +4. Python 스크립트 실행 (worktree 생성 + 설정 파일 복사) +5. 임시 파일 자동 삭제 +6. 결과 출력 + +--- + +사용자 입력에서 두 번째 줄을 추출하여 브랜치명으로 사용하세요. + +브랜치명이 제공되지 않은 경우: +- 사용법을 안내하세요. + +브랜치명이 제공된 경우: +1. 프로젝트 루트로 이동 +2. Git 긴 경로 지원 활성화: `git config --global core.longpaths true` (최초 1회만 실행) +3. 브랜치명에서 `#` 문자 제거 (예: `20260116_#432_...` → `20260116_432_...`) +4. 임시 Python 스크립트 파일 생성: + - 파일명: `init_worktree_temp_{timestamp}.py` + - 브랜치명을 코드에 직접 포함 (인코딩 문제 해결) + - worktree 생성 로직 포함 +5. **Python 스크립트 실행** (Windows에서는 `-X utf8` 플래그 필수): + ```bash + python -X utf8 init_worktree_temp_{timestamp}.py + ``` +6. 임시 파일 삭제 +7. 결과 출력 +8. 에이전트가 `.gitignore` 분석 후 민감 파일 복사 + +**중요**: +- **브랜치명 처리**: `#` 문자는 Git 브랜치명에서 제거됩니다 (문제 방지) +- **인코딩 문제 해결**: Python 스크립트 파일에 브랜치명을 직접 포함시켜 Windows PowerShell 인코딩 문제 회피 +- **Windows UTF-8 모드**: Python 실행 시 `-X utf8` 플래그 사용 필수 +- **설정 파일 자동 복사**: worktree 생성 후 에이전트가 동적으로 파일 복사 +- **플랫폼 독립성**: Windows/macOS/Linux 모두 동일한 방식으로 처리 + +**실행 예시**: +```powershell +# Windows PowerShell +cd d:\0-suh\project\RomRom-FE +git config --global core.longpaths true + +# Python UTF-8 모드로 실행 (Windows 한글 인코딩 문제 해결) +python -X utf8 init_worktree_temp.py + +# 브랜치명: 20260116_#432_UX_개선_및_페이지_디자인_수정 +# → Git 브랜치: 20260116_432_UX_개선_및_페이지_디자인_수정 +# → 폴더명: 20260116_432_UX_개선_및_페이지_디자인_수정 +``` + +**Python 스크립트 구조**: +```python +# -*- coding: utf-8 -*- +import sys +import os +import shutil +import glob + +# 프로젝트 루트로 이동 +os.chdir('프로젝트_루트_경로') + +# 브랜치명 (# 제거됨) +branch_name = '20260116_432_UX_개선_및_페이지_디자인_수정' + +# worktree_manager 실행 +sys.path.insert(0, '.cursor/scripts') +import worktree_manager +os.environ['GIT_BRANCH_NAME'] = branch_name +os.environ['PYTHONIOENCODING'] = 'utf-8' +sys.argv = ['worktree_manager.py'] +exit_code = worktree_manager.main() + +# worktree 경로를 환경변수로 설정 (에이전트가 파일 복사에 사용) +if exit_code == 0: + import subprocess + result = subprocess.run(['git', 'worktree', 'list', '--porcelain'], + capture_output=True, text=True, encoding='utf-8') + lines = result.stdout.split('\n') + worktree_path = None + for i, line in enumerate(lines): + if line.startswith(f'branch refs/heads/{branch_name}'): + worktree_path = lines[i-1].replace('worktree ', '') + break + + if worktree_path: + print(f'📍 WORKTREE_PATH={worktree_path}') + +sys.exit(exit_code) +``` + +## 설정 파일 복사 (에이전트 동적 판단) + +Worktree 생성 성공 후, **에이전트가 `.gitignore`를 분석하여 민감 파일을 동적으로 판단**하고 복사합니다. + +### Step 1: .gitignore 분석 + +프로젝트 `.gitignore` 파일을 읽고 다음 카테고리의 민감 파일 패턴을 식별합니다: + +| 카테고리 | 식별 패턴 | 설명 | +|---------|----------|------| +| Firebase 설정 | `google-services.json`, `GoogleService-Info.plist` | Firebase 연동 설정 | +| 서명 키/인증서 | `key.properties`, `*.jks`, `*.p12`, `*.p8`, `*.mobileprovision` | 앱 서명 인증서 | +| 빌드 설정 | `Secrets.xcconfig`, 민감한 `*.xcconfig` | iOS 빌드 비밀 설정 | +| 환경 변수 | `*.env` | 환경별 설정 파일 | +| IDE 로컬 설정 | `settings.local.json` | Claude/Cursor 로컬 설정 | + +### Step 2: 실제 파일 확인 및 복사 + +1. `.gitignore`에 명시된 패턴 중 **실제 존재하는 파일** 확인 +2. 존재하는 파일만 worktree 경로로 복사 +3. 디렉토리 구조 유지 (예: `android/app/google-services.json` → `worktree/android/app/google-services.json`) + +**복사 명령 예시**: +```bash +# Python shutil 사용 +import shutil +shutil.copy2('원본경로', 'worktree경로/원본경로') +``` + +### Step 3: 복사 제외 대상 (절대 복사 금지) + +다음은 민감 파일이더라도 **절대 복사하지 않습니다**: + +| 경로/패턴 | 이유 | +|----------|------| +| `build/`, `target/`, `.gradle/` | 빌드 산출물 (새로 빌드 필요) | +| `node_modules/`, `Pods/`, `.dart_tool/` | 의존성 (새로 설치 필요) | +| `.report/`, `.run/` | 보고서 (worktree별로 별도 생성) | +| `.idea/` | IDE 캐시 전체 | +| `*.log`, `*.class`, `*.pyc` | 임시/컴파일 파일 | + +### Step 4: 결과 출력 + +복사된 파일 목록을 ✅ 이모지와 함께 출력합니다: +``` +✅ android/app/google-services.json 복사 완료 +✅ ios/Runner/GoogleService-Info.plist 복사 완료 +✅ android/key.properties 복사 완료 +``` + +**참고**: +- 파일이 존재하지 않으면 해당 복사는 자동으로 건너뜁니다. +- 에이전트가 `.gitignore`를 분석하여 복사 대상을 동적으로 결정합니다. diff --git a/.claude/commands/report.md b/.claude/commands/report.md index 3172643..9055108 100644 --- a/.claude/commands/report.md +++ b/.claude/commands/report.md @@ -12,6 +12,52 @@ - ✅ 특이한 부분이나 주의할 점은 간단히 설명 - ✅ 민감 정보 마스킹: API Key, Password, Token, Secret 등은 반드시 마스킹 처리 +## ⛔ 금지 사항 (필수) + +보고서 작성 시 다음 내용은 **절대 포함하지 않음**: + +### 절대 사용 금지 필드 (메타 정보) +다음 필드는 어떤 형태로든 보고서에 포함하지 않음: + +- ❌ `**작성자**:` / `**작업자**:` / `**담당자**:` 필드 +- ❌ `**작성일**:` / `**작업일**:` / `**날짜**:` 필드 (보고서 본문에서) +- ❌ `## 작성 정보` 같은 메타 정보 섹션 +- ❌ 보고서 상단에 이슈 링크 + 날짜 + 작성자 조합 + +### 절대 사용 금지 단어/표현 +- ❌ `Claude`, `Claude Code`, `claude-opus`, `claude-sonnet` 등 AI 이름 +- ❌ `Cursor`, `Copilot`, `GPT`, `ChatGPT` 등 AI 도구명 +- ❌ `AI`, `인공지능`, `자동 생성`, `자동 작성` 등의 표현 +- ❌ `Co-Authored-By: Claude` 같은 서명 +- ❌ 모델명 (Sonnet, Opus, GPT-4 등) + +### 잘못된 예시 (절대 사용 금지) +```markdown +## 작성 정보 +- **작성일**: 2026-01-20 +- **작성자**: Claude Code (claude-opus-4-5-20251101) + +**이슈**: [#152](...) +**작성일**: 2026-01-12 +**작성자**: Claude Code + +**이슈**: [#145](...) +**작업일**: 2026-01-12 +**작업자**: Claude Code +``` + +### 올바른 예시 +보고서는 **작업 내용만** 포함하고, 메타 정보(작성자/작성일)는 완전히 제외: +```markdown +### 📌 작업 개요 +[작업 내용 설명] + +### ✅ 구현 내용 +[구현 내용...] +``` + +**핵심**: 보고서에는 **작성자/작성일 관련 필드를 절대 포함하지 않음**. 파일명에 날짜가 포함되므로 별도 기록 불필요. + ## 🔒 민감 정보 마스킹 (필수) 보고서 작성 시 다음 민감 정보는 반드시 마스킹 처리: diff --git a/.claude/scripts/README.md b/.claude/scripts/README.md new file mode 100644 index 0000000..05b666f --- /dev/null +++ b/.claude/scripts/README.md @@ -0,0 +1,54 @@ +# Claude Scripts + +이 디렉토리는 Claude에서 공통으로 사용할 수 있는 Python 유틸리티 스크립트를 포함합니다. + +## 📦 포함된 모듈 + +### `worktree_manager.py` (v1.0.0) + +Git worktree를 자동으로 생성하고 관리하는 스크립트입니다. + +#### 기능 +- 브랜치가 없으면 자동 생성 (현재 브랜치에서 분기) +- 브랜치명의 특수문자(`#`, `/`, `\` 등)를 안전하게 처리 +- `RomRom-Worktree` 폴더에 worktree 자동 생성 +- 이미 존재하는 worktree는 건너뛰고 경로만 출력 + +#### 사용법 + +**직접 실행:** + +```bash +python .claude/scripts/worktree_manager.py "20260120_#163_Github_Projects_에_대한_템플릿_개발_필요" +``` + +#### 출력 예시 + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🌿 Git Worktree Manager v1.0.0 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📋 입력된 브랜치: 20260120_#163_Github_Projects_에_대한_템플릿_개발_필요 +📁 폴더명: 20260120_163_Github_Projects_에_대한_템플릿_개발_필요 + +🔍 브랜치 확인 중... +⚠️ 브랜치가 존재하지 않습니다. +🔄 현재 브랜치(main)에서 새 브랜치 생성 중... +✅ 브랜치 생성 완료! + +📂 Worktree 경로: /Users/.../project/RomRom-Worktree/20260120_163_Github_Projects_에_대한_템플릿_개발_필요 + +🔄 Worktree 생성 중... +✅ Worktree 생성 완료! + +📍 경로: /Users/.../project/RomRom-Worktree/20260120_163_Github_Projects_에_대한_템플릿_개발_필요 +``` + +## ✅ 장점 + +- 🌏 **한글 경로 완벽 지원**: UTF-8 인코딩으로 저장되어 안전 +- 🔄 **재사용 가능**: 모든 프로젝트에서 사용 가능 +- 📝 **영구 보관**: 삭제되지 않고 계속 사용 가능 +- 🤖 **자동화**: 브랜치 생성부터 worktree 생성까지 자동화 +- 📚 **문서화**: 모든 함수에 docstring 포함 diff --git a/.cursor/commands/init-workflow.md b/.cursor/commands/init-workflow.md new file mode 100644 index 0000000..6ce2d84 --- /dev/null +++ b/.cursor/commands/init-workflow.md @@ -0,0 +1,166 @@ +# init-workflow + +Git worktree를 자동으로 생성하는 커맨드입니다. + +브랜치명을 입력받아 자동으로: +1. 브랜치명에서 `#` 문자 제거 (Git 브랜치명으로 사용) +2. 브랜치가 없으면 생성 (현재 브랜치에서 분기) +3. 브랜치명의 특수문자를 `_`로 변환하여 폴더명 생성 +4. `{프로젝트명}-Worktree` 폴더에 worktree 생성 (예: `RomRom-FE-Worktree`) +5. 설정 파일 자동 복사 (Firebase, iOS, Android 키 등) +6. 이미 존재하면 경로만 출력 + +## 사용법 + +``` +/init-workflow + +20260120_#163_Github_Projects_에_대한_템플릿_개발_필요_및_관련_Sync_워크플로우_개발_필요 +``` + +## 실행 로직 + +1. 사용자 입력에서 두 번째 줄의 브랜치명 추출 +2. 브랜치명에서 `#` 문자 제거 +3. 임시 Python 스크립트 파일 생성 (인코딩 문제 해결) +4. Python 스크립트 실행 (worktree 생성 + 설정 파일 복사) +5. 임시 파일 자동 삭제 +6. 결과 출력 + +--- + +사용자 입력에서 두 번째 줄을 추출하여 브랜치명으로 사용하세요. + +브랜치명이 제공되지 않은 경우: +- 사용법을 안내하세요. + +브랜치명이 제공된 경우: +1. 프로젝트 루트로 이동 +2. Git 긴 경로 지원 활성화: `git config --global core.longpaths true` (최초 1회만 실행) +3. 브랜치명에서 `#` 문자 제거 (예: `20260116_#432_...` → `20260116_432_...`) +4. 임시 Python 스크립트 파일 생성: + - 파일명: `init_worktree_temp_{timestamp}.py` + - 브랜치명을 코드에 직접 포함 (인코딩 문제 해결) + - worktree 생성 로직 포함 +5. **Python 스크립트 실행** (Windows에서는 `-X utf8` 플래그 필수): + ```bash + python -X utf8 init_worktree_temp_{timestamp}.py + ``` +6. 임시 파일 삭제 +7. 결과 출력 +8. 에이전트가 `.gitignore` 분석 후 민감 파일 복사 + +**중요**: +- **브랜치명 처리**: `#` 문자는 Git 브랜치명에서 제거됩니다 (문제 방지) +- **인코딩 문제 해결**: Python 스크립트 파일에 브랜치명을 직접 포함시켜 Windows PowerShell 인코딩 문제 회피 +- **Windows UTF-8 모드**: Python 실행 시 `-X utf8` 플래그 사용 필수 +- **설정 파일 자동 복사**: worktree 생성 후 에이전트가 동적으로 파일 복사 +- **플랫폼 독립성**: Windows/macOS/Linux 모두 동일한 방식으로 처리 + +**실행 예시**: +```powershell +# Windows PowerShell +cd d:\0-suh\project\RomRom-FE +git config --global core.longpaths true + +# Python UTF-8 모드로 실행 (Windows 한글 인코딩 문제 해결) +python -X utf8 init_worktree_temp.py + +# 브랜치명: 20260116_#432_UX_개선_및_페이지_디자인_수정 +# → Git 브랜치: 20260116_432_UX_개선_및_페이지_디자인_수정 +# → 폴더명: 20260116_432_UX_개선_및_페이지_디자인_수정 +``` + +**Python 스크립트 구조**: +```python +# -*- coding: utf-8 -*- +import sys +import os +import shutil +import glob + +# 프로젝트 루트로 이동 +os.chdir('프로젝트_루트_경로') + +# 브랜치명 (# 제거됨) +branch_name = '20260116_432_UX_개선_및_페이지_디자인_수정' + +# worktree_manager 실행 +sys.path.insert(0, '.cursor/scripts') +import worktree_manager +os.environ['GIT_BRANCH_NAME'] = branch_name +os.environ['PYTHONIOENCODING'] = 'utf-8' +sys.argv = ['worktree_manager.py'] +exit_code = worktree_manager.main() + +# worktree 경로를 환경변수로 설정 (에이전트가 파일 복사에 사용) +if exit_code == 0: + import subprocess + result = subprocess.run(['git', 'worktree', 'list', '--porcelain'], + capture_output=True, text=True, encoding='utf-8') + lines = result.stdout.split('\n') + worktree_path = None + for i, line in enumerate(lines): + if line.startswith(f'branch refs/heads/{branch_name}'): + worktree_path = lines[i-1].replace('worktree ', '') + break + + if worktree_path: + print(f'📍 WORKTREE_PATH={worktree_path}') + +sys.exit(exit_code) +``` + +## 설정 파일 복사 (에이전트 동적 판단) + +Worktree 생성 성공 후, **에이전트가 `.gitignore`를 분석하여 민감 파일을 동적으로 판단**하고 복사합니다. + +### Step 1: .gitignore 분석 + +프로젝트 `.gitignore` 파일을 읽고 다음 카테고리의 민감 파일 패턴을 식별합니다: + +| 카테고리 | 식별 패턴 | 설명 | +|---------|----------|------| +| Firebase 설정 | `google-services.json`, `GoogleService-Info.plist` | Firebase 연동 설정 | +| 서명 키/인증서 | `key.properties`, `*.jks`, `*.p12`, `*.p8`, `*.mobileprovision` | 앱 서명 인증서 | +| 빌드 설정 | `Secrets.xcconfig`, 민감한 `*.xcconfig` | iOS 빌드 비밀 설정 | +| 환경 변수 | `*.env` | 환경별 설정 파일 | +| IDE 로컬 설정 | `settings.local.json` | Claude/Cursor 로컬 설정 | + +### Step 2: 실제 파일 확인 및 복사 + +1. `.gitignore`에 명시된 패턴 중 **실제 존재하는 파일** 확인 +2. 존재하는 파일만 worktree 경로로 복사 +3. 디렉토리 구조 유지 (예: `android/app/google-services.json` → `worktree/android/app/google-services.json`) + +**복사 명령 예시**: +```bash +# Python shutil 사용 +import shutil +shutil.copy2('원본경로', 'worktree경로/원본경로') +``` + +### Step 3: 복사 제외 대상 (절대 복사 금지) + +다음은 민감 파일이더라도 **절대 복사하지 않습니다**: + +| 경로/패턴 | 이유 | +|----------|------| +| `build/`, `target/`, `.gradle/` | 빌드 산출물 (새로 빌드 필요) | +| `node_modules/`, `Pods/`, `.dart_tool/` | 의존성 (새로 설치 필요) | +| `.report/`, `.run/` | 보고서 (worktree별로 별도 생성) | +| `.idea/` | IDE 캐시 전체 | +| `*.log`, `*.class`, `*.pyc` | 임시/컴파일 파일 | + +### Step 4: 결과 출력 + +복사된 파일 목록을 ✅ 이모지와 함께 출력합니다: +``` +✅ android/app/google-services.json 복사 완료 +✅ ios/Runner/GoogleService-Info.plist 복사 완료 +✅ android/key.properties 복사 완료 +``` + +**참고**: +- 파일이 존재하지 않으면 해당 복사는 자동으로 건너뜁니다. +- 에이전트가 `.gitignore`를 분석하여 복사 대상을 동적으로 결정합니다. diff --git a/.cursor/commands/report.md b/.cursor/commands/report.md index 3172643..9055108 100644 --- a/.cursor/commands/report.md +++ b/.cursor/commands/report.md @@ -12,6 +12,52 @@ - ✅ 특이한 부분이나 주의할 점은 간단히 설명 - ✅ 민감 정보 마스킹: API Key, Password, Token, Secret 등은 반드시 마스킹 처리 +## ⛔ 금지 사항 (필수) + +보고서 작성 시 다음 내용은 **절대 포함하지 않음**: + +### 절대 사용 금지 필드 (메타 정보) +다음 필드는 어떤 형태로든 보고서에 포함하지 않음: + +- ❌ `**작성자**:` / `**작업자**:` / `**담당자**:` 필드 +- ❌ `**작성일**:` / `**작업일**:` / `**날짜**:` 필드 (보고서 본문에서) +- ❌ `## 작성 정보` 같은 메타 정보 섹션 +- ❌ 보고서 상단에 이슈 링크 + 날짜 + 작성자 조합 + +### 절대 사용 금지 단어/표현 +- ❌ `Claude`, `Claude Code`, `claude-opus`, `claude-sonnet` 등 AI 이름 +- ❌ `Cursor`, `Copilot`, `GPT`, `ChatGPT` 등 AI 도구명 +- ❌ `AI`, `인공지능`, `자동 생성`, `자동 작성` 등의 표현 +- ❌ `Co-Authored-By: Claude` 같은 서명 +- ❌ 모델명 (Sonnet, Opus, GPT-4 등) + +### 잘못된 예시 (절대 사용 금지) +```markdown +## 작성 정보 +- **작성일**: 2026-01-20 +- **작성자**: Claude Code (claude-opus-4-5-20251101) + +**이슈**: [#152](...) +**작성일**: 2026-01-12 +**작성자**: Claude Code + +**이슈**: [#145](...) +**작업일**: 2026-01-12 +**작업자**: Claude Code +``` + +### 올바른 예시 +보고서는 **작업 내용만** 포함하고, 메타 정보(작성자/작성일)는 완전히 제외: +```markdown +### 📌 작업 개요 +[작업 내용 설명] + +### ✅ 구현 내용 +[구현 내용...] +``` + +**핵심**: 보고서에는 **작성자/작성일 관련 필드를 절대 포함하지 않음**. 파일명에 날짜가 포함되므로 별도 기록 불필요. + ## 🔒 민감 정보 마스킹 (필수) 보고서 작성 시 다음 민감 정보는 반드시 마스킹 처리: diff --git a/.cursor/scripts/README.md b/.cursor/scripts/README.md index fcae808..9e558be 100644 --- a/.cursor/scripts/README.md +++ b/.cursor/scripts/README.md @@ -24,6 +24,57 @@ Cursor command에서 PowerShell 명령어 실행 시 오류가 발생하면, 이 ## 📦 포함된 모듈 +### `worktree_manager.py` (v1.0.0) 🆕 + +Git worktree를 자동으로 생성하고 관리하는 스크립트입니다. + +#### 기능 +- 브랜치가 없으면 자동 생성 (현재 브랜치에서 분기) +- 브랜치명의 특수문자(`#`, `/`, `\` 등)를 안전하게 처리 +- `RomRom-Worktree` 폴더에 worktree 자동 생성 +- 이미 존재하는 worktree는 건너뛰고 경로만 출력 + +#### 사용법 + +**Cursor Command로 실행 (권장):** + +``` +/init-workflow + +20260120_#163_Github_Projects_에_대한_템플릿_개발_필요 +``` + +**직접 실행:** + +```bash +python .cursor/scripts/worktree_manager.py "20260120_#163_Github_Projects_에_대한_템플릿_개발_필요" +``` + +#### 출력 예시 + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🌿 Git Worktree Manager v1.0.0 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📋 입력된 브랜치: 20260120_#163_Github_Projects_에_대한_템플릿_개발_필요 +📁 폴더명: 20260120_163_Github_Projects_에_대한_템플릿_개발_필요 + +🔍 브랜치 확인 중... +⚠️ 브랜치가 존재하지 않습니다. +🔄 현재 브랜치(main)에서 새 브랜치 생성 중... +✅ 브랜치 생성 완료! + +📂 Worktree 경로: /Users/.../project/RomRom-Worktree/20260120_163_Github_Projects_에_대한_템플릿_개발_필요 + +🔄 Worktree 생성 중... +✅ Worktree 생성 완료! + +📍 경로: /Users/.../project/RomRom-Worktree/20260120_163_Github_Projects_에_대한_템플릿_개발_필요 +``` + +--- + ### `common_util.py` (v1.0.0) 한글 경로 처리 등 공통 기능을 제공하는 유틸리티 모듈입니다. diff --git a/.cursor/scripts/worktree_manager.py b/.cursor/scripts/worktree_manager.py new file mode 100644 index 0000000..fe2dbe0 --- /dev/null +++ b/.cursor/scripts/worktree_manager.py @@ -0,0 +1,644 @@ +# -*- coding: utf-8 -*- +""" +Git Worktree Manager v1.0.4 + +Git worktree를 자동으로 생성하고 관리하는 스크립트입니다. +브랜치가 없으면 자동으로 생성하고, 브랜치명의 특수문자를 안전하게 처리합니다. + +사용법: + macOS/Linux: + python worktree_manager.py + + Windows (환경 변수 방식, 권장): + $env:GIT_BRANCH_NAME = "브랜치명" + $env:PYTHONIOENCODING = "utf-8" + python -X utf8 worktree_manager.py + +예시: + python worktree_manager.py "20260120_#163_Github_Projects_에_대한_템플릿_개발_필요" + +Author: Cursor AI Assistant +Version: 1.0.4 +""" + +import os +import sys +import subprocess +import re +import platform +import io +from pathlib import Path +from typing import Dict, Optional, Tuple + +# Windows 인코딩 문제 해결 - stdout/stderr를 UTF-8로 래핑 +if platform.system() == 'Windows': + try: + # stdout/stderr가 버퍼를 가지고 있는 경우에만 래핑 + if hasattr(sys.stdout, 'buffer'): + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + if hasattr(sys.stderr, 'buffer'): + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + except Exception: + pass # 래핑 실패 시 무시 + + +# =================================================================== +# 상수 정의 +# =================================================================== + +VERSION = "1.0.4" + +# Windows 환경 감지 +IS_WINDOWS = platform.system() == 'Windows' + +# 폴더명에서 제거할 특수문자 (파일시스템에서 안전하지 않은 문자) +SPECIAL_CHARS_PATTERN = r'[#/\\:*?"<>|]' + +# Worktree 루트 폴더명 (동적으로 설정됨) +# 예: RomRom-FE → RomRom-FE-Worktree +WORKTREE_ROOT_NAME = None # get_worktree_root()에서 동적으로 설정 + + +# =================================================================== +# 유틸리티 함수 +# =================================================================== + +def get_branch_name() -> str: + """ + 브랜치명을 안전하게 받기 (Windows 인코딩 문제 해결) + + Windows 환경에서 PowerShell → Python 스크립트로 한글 브랜치명을 전달할 때 + 인코딩 문제가 발생하므로, 환경 변수나 파일에서 읽는 방식을 우선 사용합니다. + + Returns: + str: 브랜치명 (비어있을 수 있음) + """ + if IS_WINDOWS: + # 방법 1: 환경 변수에서 읽기 (가장 간단하고 안전) + # Windows에서 환경 변수는 시스템 기본 인코딩을 사용하므로 UTF-8로 디코딩 시도 + branch_name_raw = os.environ.get('GIT_BRANCH_NAME', '') + if branch_name_raw: + try: + # 환경 변수가 이미 올바른 인코딩인 경우 + branch_name = branch_name_raw.strip() + # 한글이 깨져있는지 확인 (깨진 경우 복구 시도) + if '\xef\xbf\xbd' in branch_name.encode('utf-8', errors='replace').decode('utf-8', errors='replace'): + # 깨진 경우, 시스템 인코딩으로 디코딩 후 UTF-8로 재인코딩 시도 + import locale + sys_encoding = locale.getpreferredencoding() + branch_name = branch_name_raw.encode(sys_encoding, errors='replace').decode('utf-8', errors='replace').strip() + else: + branch_name = branch_name.strip() + if branch_name: + return branch_name + except Exception: + # 인코딩 변환 실패 시 원본 사용 + branch_name = branch_name_raw.strip() + if branch_name: + return branch_name + + # 방법 2: 임시 파일에서 읽기 (init-workflow에서 파일 생성 후 전달) + temp_file = os.environ.get('BRANCH_NAME_FILE', '') + if temp_file and os.path.exists(temp_file): + try: + # 여러 인코딩 시도: UTF-8, UTF-8 with BOM, 시스템 기본 인코딩 + encodings = ['utf-8', 'utf-8-sig', 'cp949', 'euc-kr'] + branch_name = None + for encoding in encodings: + try: + with open(temp_file, 'r', encoding=encoding) as f: + branch_name = f.read().strip() + if branch_name and not any(ord(c) > 0xFFFF for c in branch_name if ord(c) > 0x7F): + # 한글이 제대로 읽혔는지 확인 (깨진 문자가 없는지) + break + except (UnicodeDecodeError, UnicodeError): + continue + + if branch_name: + return branch_name + except Exception as e: + print_warning(f"브랜치명 파일 읽기 실패: {e}") + + # 방법 3: stdin에서 읽기 시도 (파이프 입력인 경우) + if not sys.stdin.isatty(): + try: + branch_name = sys.stdin.read().strip() + if branch_name: + return branch_name + except Exception: + pass + + # 기본: sys.argv에서 받기 (macOS/Linux 또는 Windows에서도 인자로 전달된 경우) + if len(sys.argv) >= 2: + return sys.argv[1].strip() + + return '' + + +def print_header(): + """헤더 출력""" + print("━" * 60) + print(f"🌿 Git Worktree Manager v{VERSION}") + print("━" * 60) + print() + + +def print_step(emoji: str, message: str): + """단계별 메시지 출력""" + print(f"{emoji} {message}") + + +def print_error(message: str): + """에러 메시지 출력""" + print(f"❌ 에러: {message}", file=sys.stderr) + + +def print_success(message: str): + """성공 메시지 출력""" + print(f"✅ {message}") + + +def print_info(message: str): + """정보 메시지 출력""" + print(f"ℹ️ {message}") + + +def print_warning(message: str): + """경고 메시지 출력""" + print(f"⚠️ {message}") + + +# =================================================================== +# Git 관련 함수 +# =================================================================== + +def run_git_command(args: list, check: bool = True) -> Tuple[bool, str, str]: + """ + Git 명령어 실행 + + Args: + args: Git 명령어 인자 리스트 (예: ['branch', '--list']) + check: 에러 발생 시 예외를 발생시킬지 여부 + + Returns: + Tuple[bool, str, str]: (성공 여부, stdout, stderr) + """ + try: + result = subprocess.run( + ['git'] + args, + capture_output=True, + text=True, + encoding='utf-8', + check=check + ) + return True, result.stdout.strip(), result.stderr.strip() + except subprocess.CalledProcessError as e: + return False, e.stdout.strip() if e.stdout else "", e.stderr.strip() if e.stderr else "" + except Exception as e: + return False, "", str(e) + + +def check_and_enable_longpaths() -> bool: + """ + Windows에서 Git 긴 경로 지원 확인 및 활성화 + + Returns: + bool: 긴 경로 지원이 활성화되어 있으면 True + """ + if not IS_WINDOWS: + return True + + # 현재 설정 확인 + success, stdout, _ = run_git_command(['config', '--global', 'core.longpaths'], check=False) + if success and stdout.strip().lower() == 'true': + return True + + # 긴 경로 지원 활성화 + print_info("Windows 긴 경로 지원을 활성화합니다...") + success, _, stderr = run_git_command(['config', '--global', 'core.longpaths', 'true'], check=False) + if success: + print_success("긴 경로 지원이 활성화되었습니다.") + return True + else: + print_warning(f"긴 경로 지원 활성화 실패: {stderr}") + print_warning("수동으로 실행하세요: git config --global core.longpaths true") + return False + + +def is_git_repository() -> bool: + """현재 디렉토리가 Git 저장소인지 확인""" + success, _, _ = run_git_command(['rev-parse', '--git-dir'], check=False) + return success + + +def get_git_root() -> Optional[Path]: + """Git 저장소 루트 경로 반환""" + success, stdout, _ = run_git_command(['rev-parse', '--show-toplevel'], check=False) + if success and stdout: + return Path(stdout) + return None + + +def get_current_branch() -> Optional[str]: + """현재 체크아웃된 브랜치명 반환""" + success, stdout, _ = run_git_command(['branch', '--show-current'], check=False) + if success and stdout: + return stdout + return None + + +def branch_exists(branch_name: str) -> bool: + """ + 브랜치 존재 여부 확인 + + Args: + branch_name: 확인할 브랜치명 + + Returns: + bool: 브랜치가 존재하면 True + """ + success, stdout, _ = run_git_command(['branch', '--list', branch_name], check=False) + if success and stdout: + # 출력 형식: " branch_name" 또는 "* branch_name" + branches = [line.strip().lstrip('* ') for line in stdout.split('\n')] + return branch_name in branches + return False + + +def create_branch(branch_name: str) -> bool: + """ + 현재 브랜치에서 새 브랜치 생성 + + Args: + branch_name: 생성할 브랜치명 + + Returns: + bool: 성공 여부 + """ + success, _, stderr = run_git_command(['branch', branch_name], check=False) + if not success: + print_error(f"브랜치 생성 실패: {stderr}") + return success + + +def get_worktree_list() -> Dict[str, str]: + """ + 현재 등록된 worktree 목록 반환 + + Returns: + Dict[str, str]: {worktree_path: branch_name} + """ + success, stdout, _ = run_git_command(['worktree', 'list', '--porcelain'], check=False) + if not success: + return {} + + worktrees = {} + current_path = None + + for line in stdout.split('\n'): + if line.startswith('worktree '): + current_path = line.replace('worktree ', '') + elif line.startswith('branch '): + branch = line.replace('branch ', '').replace('refs/heads/', '') + if current_path: + worktrees[current_path] = branch + current_path = None + + return worktrees + + +def prune_worktrees() -> bool: + """ + 유효하지 않은 worktree 정리 (git worktree prune) + + Returns: + bool: 성공 여부 + """ + success, _, stderr = run_git_command(['worktree', 'prune'], check=False) + if not success: + print_warning(f"Worktree prune 실패: {stderr}") + return success + + +def is_worktree_exists(worktree_path: Path) -> bool: + """ + 특정 경로에 worktree가 이미 존재하는지 확인 + + Git worktree 목록과 실제 디렉토리 존재 여부를 모두 확인합니다. + prunable 상태의 worktree는 자동으로 정리합니다. + + Args: + worktree_path: 확인할 worktree 경로 + + Returns: + bool: worktree가 유효하게 존재하면 True + """ + # 먼저 prunable worktree 정리 + prune_worktrees() + + worktrees = get_worktree_list() + worktree_path_resolved = worktree_path.resolve() + + for path in worktrees.keys(): + if Path(path).resolve() == worktree_path_resolved: + # Git 목록에 있으면 실제 디렉토리도 존재하는지 확인 + if Path(path).exists(): + return True + else: + # 디렉토리가 없으면 다시 prune 실행 + print_warning(f"Worktree 경로가 존재하지 않아 정리합니다: {path}") + prune_worktrees() + return False + + # 디렉토리만 존재하고 Git에 등록되지 않은 경우도 확인 + if worktree_path_resolved.exists(): + # .git 파일이 있는지 확인 (worktree의 특징) + git_file = worktree_path_resolved / '.git' + if git_file.exists(): + print_warning(f"디렉토리가 존재하지만 Git에 등록되지 않음: {worktree_path}") + return True + + return False + + +def create_worktree(branch_name: str, worktree_path: Path) -> Dict: + """ + Git worktree 생성 + + Args: + branch_name: 체크아웃할 브랜치명 + worktree_path: worktree를 생성할 경로 + + Returns: + Dict: { + 'success': bool, + 'path': str, + 'message': str, + 'is_existing': bool + } + """ + # 이미 존재하는지 확인 + if is_worktree_exists(worktree_path): + return { + 'success': True, + 'path': str(worktree_path.resolve()), + 'message': 'Worktree가 이미 존재합니다.', + 'is_existing': True + } + + # worktree 생성 + success, stdout, stderr = run_git_command( + ['worktree', 'add', str(worktree_path), branch_name], + check=False + ) + + if success: + return { + 'success': True, + 'path': str(worktree_path.resolve()), + 'message': 'Worktree 생성 완료!', + 'is_existing': False + } + else: + return { + 'success': False, + 'path': str(worktree_path.resolve()), + 'message': f'Worktree 생성 실패: {stderr}', + 'is_existing': False + } + + +# =================================================================== +# 경로 관련 함수 +# =================================================================== + +def normalize_branch_name(branch_name: str) -> str: + """ + 브랜치명을 폴더명으로 안전하게 변환 + + 특수문자 (#, /, \\, :, *, ?, ", <, >, |)를 _ 로 변환하고, + 연속된 _를 하나로 통합하며, 앞뒤 _를 제거합니다. + + Args: + branch_name: 원본 브랜치명 + + Returns: + str: 정규화된 폴더명 + + Example: + >>> normalize_branch_name("20260120_#163_Github_Projects") + "20260120_163_Github_Projects" + """ + # 특수문자를 _ 로 변환 + normalized = re.sub(SPECIAL_CHARS_PATTERN, '_', branch_name) + + # 연속된 _를 하나로 통합 + normalized = re.sub(r'_+', '_', normalized) + + # 앞뒤 _를 제거 + normalized = normalized.strip('_') + + return normalized + + +def get_worktree_root() -> Path: + """ + Worktree 루트 경로 계산 + + 현재 Git 저장소의 부모 디렉토리에 {프로젝트명}-Worktree 폴더 생성 + + Returns: + Path: Worktree 루트 경로 + + Example: + 현재: /Users/.../project/RomRom-FE + 반환: /Users/.../project/RomRom-FE-Worktree + """ + git_root = get_git_root() + if not git_root: + raise RuntimeError("Git 저장소 루트를 찾을 수 없습니다.") + + # 현재 Git 저장소의 이름 추출 (예: RomRom-FE) + project_name = git_root.name + + # 부모 디렉토리에 {프로젝트명}-Worktree 폴더 생성 + worktree_root_name = f"{project_name}-Worktree" + worktree_root = git_root.parent / worktree_root_name + + return worktree_root + + +def get_worktree_path(branch_name: str) -> Path: + """ + 특정 브랜치의 worktree 전체 경로 반환 + + Args: + branch_name: 브랜치명 (정규화 전) + + Returns: + Path: Worktree 경로 + + Example: + >>> get_worktree_path("20260120_#163_Github_Projects") + Path("/Users/.../project/RomRom-FE-Worktree/20260120_163_Github_Projects") + """ + worktree_root = get_worktree_root() + folder_name = normalize_branch_name(branch_name) + return worktree_root / folder_name + + +def ensure_directory(path: Path) -> bool: + """ + 디렉토리가 존재하지 않으면 생성 + + Args: + path: 생성할 디렉토리 경로 + + Returns: + bool: 성공 여부 + """ + try: + path.mkdir(parents=True, exist_ok=True) + return True + except Exception as e: + print_error(f"디렉토리 생성 실패: {e}") + return False + + +# =================================================================== +# 메인 워크플로우 +# =================================================================== + +def main() -> int: + """ + 메인 워크플로우 + + Returns: + int: Exit code (0: 성공, 1: 실패) + """ + print_header() + + # 1. 브랜치명 받기 (Windows 환경 대응) + branch_name = get_branch_name() + + if not branch_name: + print_error("브랜치명이 제공되지 않았습니다.") + print() + print("사용법:") + if IS_WINDOWS: + print(" Windows 환경:") + print(" 방법 1: 환경 변수 사용") + print(f' $env:GIT_BRANCH_NAME = "브랜치명"') + print(f" python {sys.argv[0]}") + print() + print(" 방법 2: 파일 사용") + print(f' $env:BRANCH_NAME_FILE = "branch_name.txt"') + print(f" python {sys.argv[0]}") + print() + print(" 방법 3: 인자로 전달 (한글 깨짐 가능)") + print(f' python {sys.argv[0]} "브랜치명"') + else: + print(f" python {sys.argv[0]} ") + print() + print("예시:") + print(f' python {sys.argv[0]} "20260120_#163_Github_Projects_에_대한_템플릿_개발_필요"') + return 1 + + print_step("📋", f"입력된 브랜치: {branch_name}") + + # 2. Git 저장소 확인 + if not is_git_repository(): + print_error("현재 디렉토리가 Git 저장소가 아닙니다.") + return 1 + + # 2-1. Windows 긴 경로 지원 확인 및 활성화 + if IS_WINDOWS: + check_and_enable_longpaths() + print() + + # 3. 브랜치명 정규화 + folder_name = normalize_branch_name(branch_name) + print_step("📁", f"폴더명: {folder_name}") + print() + + # 4. 브랜치 존재 확인 + print_step("🔍", "브랜치 확인 중...") + + if not branch_exists(branch_name): + print_warning("브랜치가 존재하지 않습니다.") + + current_branch = get_current_branch() + if current_branch: + print_step("🔄", f"현재 브랜치({current_branch})에서 새 브랜치 생성 중...") + else: + print_step("🔄", "새 브랜치 생성 중...") + + if not create_branch(branch_name): + print_error("브랜치 생성에 실패했습니다.") + return 1 + + print_success("브랜치 생성 완료!") + else: + print_success("브랜치가 이미 존재합니다.") + + print() + + # 5. Worktree 경로 계산 + try: + worktree_path = get_worktree_path(branch_name) + except RuntimeError as e: + print_error(str(e)) + return 1 + + print_step("📂", f"Worktree 경로: {worktree_path}") + print() + + # 6. Worktree 존재 확인 + print_step("🔍", "Worktree 확인 중...") + + if is_worktree_exists(worktree_path): + print_info("Worktree가 이미 존재합니다.") + print() + print_step("📍", f"경로: {worktree_path.resolve()}") + return 0 + + # 7. Worktree 루트 디렉토리 생성 + worktree_root = get_worktree_root() + if not ensure_directory(worktree_root): + return 1 + + # 8. Worktree 생성 + print_step("🔄", "Worktree 생성 중...") + + result = create_worktree(branch_name, worktree_path) + + if result['success']: + if result['is_existing']: + print_info(result['message']) + else: + print_success(result['message']) + + print() + print_step("📍", f"경로: {result['path']}") + return 0 + else: + print_error(result['message']) + return 1 + + +# =================================================================== +# 엔트리 포인트 +# =================================================================== + +if __name__ == "__main__": + try: + exit_code = main() + sys.exit(exit_code) + except KeyboardInterrupt: + print() + print_warning("사용자에 의해 중단되었습니다.") + sys.exit(130) + except Exception as e: + print() + print_error(f"예상치 못한 오류가 발생했습니다: {e}") + sys.exit(1) From 9a5eaa56944b93debe82f72b909c54101acdc685 Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Tue, 27 Jan 2026 15:35:27 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20Spring=20init=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20workflow=20=EA=B5=AC=ED=98=84=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=20:=20feat=20:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20custom?= =?UTF-8?q?=20command=20=EC=B6=94=EA=B0=802=20https://github.com/MapSee-La?= =?UTF-8?q?b/MapSee-BE/issues/2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/scripts/worktree_manager.py | 621 ++++++++++++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 .claude/scripts/worktree_manager.py diff --git a/.claude/scripts/worktree_manager.py b/.claude/scripts/worktree_manager.py new file mode 100644 index 0000000..6e474e6 --- /dev/null +++ b/.claude/scripts/worktree_manager.py @@ -0,0 +1,621 @@ +# -*- coding: utf-8 -*- +""" +Git Worktree Manager v1.0.4 + +Git worktree를 자동으로 생성하고 관리하는 스크립트입니다. +브랜치가 없으면 자동으로 생성하고, 브랜치명의 특수문자를 안전하게 처리합니다. + +사용법: + macOS/Linux: + python worktree_manager.py + + Windows (환경 변수 방식, 권장): + $env:GIT_BRANCH_NAME = "브랜치명" + $env:PYTHONIOENCODING = "utf-8" + python -X utf8 worktree_manager.py + +예시: + python worktree_manager.py "20260120_#163_Github_Projects_에_대한_템플릿_개발_필요" + +Author: Cursor AI Assistant +Version: 1.0.4 +""" + +import os +import sys +import subprocess +import re +import platform +import io +from pathlib import Path +from typing import Dict, Optional, Tuple + +# Windows 인코딩 문제 해결 - stdout/stderr를 UTF-8로 래핑 +if platform.system() == 'Windows': + try: + # stdout/stderr가 버퍼를 가지고 있는 경우에만 래핑 + if hasattr(sys.stdout, 'buffer'): + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + if hasattr(sys.stderr, 'buffer'): + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + except Exception: + pass # 래핑 실패 시 무시 + + +# =================================================================== +# 상수 정의 +# =================================================================== + +VERSION = "1.0.4" + +# Windows 환경 감지 +IS_WINDOWS = platform.system() == 'Windows' + +# 폴더명에서 제거할 특수문자 (파일시스템에서 안전하지 않은 문자) +SPECIAL_CHARS_PATTERN = r'[#/\\:*?"<>|]' + +# Worktree 루트 폴더명 (동적으로 설정됨) +# 예: RomRom-FE → RomRom-FE-Worktree +WORKTREE_ROOT_NAME = None # get_worktree_root()에서 동적으로 설정 + + +# =================================================================== +# 유틸리티 함수 +# =================================================================== + +def get_branch_name() -> str: + """ + 브랜치명을 안전하게 받기 (Windows 인코딩 문제 해결) + + Windows 환경에서 PowerShell → Python 스크립트로 한글 브랜치명을 전달할 때 + 인코딩 문제가 발생하므로, 환경 변수나 파일에서 읽는 방식을 우선 사용합니다. + + Returns: + str: 브랜치명 (비어있을 수 있음) + """ + if IS_WINDOWS: + # 방법 1: 환경 변수에서 읽기 (가장 간단하고 안전) + branch_name_raw = os.environ.get('GIT_BRANCH_NAME', '') + if branch_name_raw: + try: + branch_name = branch_name_raw.strip() + if branch_name: + return branch_name + except Exception: + pass + + # 방법 2: 임시 파일에서 읽기 + temp_file = os.environ.get('BRANCH_NAME_FILE', '') + if temp_file and os.path.exists(temp_file): + try: + encodings = ['utf-8', 'utf-8-sig', 'cp949', 'euc-kr'] + for encoding in encodings: + try: + with open(temp_file, 'r', encoding=encoding) as f: + branch_name = f.read().strip() + if branch_name: + return branch_name + except (UnicodeDecodeError, UnicodeError): + continue + except Exception: + pass + + # 방법 3: stdin에서 읽기 시도 + if not sys.stdin.isatty(): + try: + branch_name = sys.stdin.read().strip() + if branch_name: + return branch_name + except Exception: + pass + + # 기본: sys.argv에서 받기 + if len(sys.argv) >= 2: + return sys.argv[1].strip() + + return '' + + +def print_header(): + """헤더 출력""" + print("━" * 60) + print(f"🌿 Git Worktree Manager v{VERSION}") + print("━" * 60) + print() + + +def print_step(emoji: str, message: str): + """단계별 메시지 출력""" + print(f"{emoji} {message}") + + +def print_error(message: str): + """에러 메시지 출력""" + print(f"❌ 에러: {message}", file=sys.stderr) + + +def print_success(message: str): + """성공 메시지 출력""" + print(f"✅ {message}") + + +def print_info(message: str): + """정보 메시지 출력""" + print(f"ℹ️ {message}") + + +def print_warning(message: str): + """경고 메시지 출력""" + print(f"⚠️ {message}") + + +# =================================================================== +# Git 관련 함수 +# =================================================================== + +def run_git_command(args: list, check: bool = True) -> Tuple[bool, str, str]: + """ + Git 명령어 실행 + + Args: + args: Git 명령어 인자 리스트 (예: ['branch', '--list']) + check: 에러 발생 시 예외를 발생시킬지 여부 + + Returns: + Tuple[bool, str, str]: (성공 여부, stdout, stderr) + """ + try: + result = subprocess.run( + ['git'] + args, + capture_output=True, + text=True, + encoding='utf-8', + check=check + ) + return True, result.stdout.strip(), result.stderr.strip() + except subprocess.CalledProcessError as e: + return False, e.stdout.strip() if e.stdout else "", e.stderr.strip() if e.stderr else "" + except Exception as e: + return False, "", str(e) + + +def check_and_enable_longpaths() -> bool: + """ + Windows에서 Git 긴 경로 지원 확인 및 활성화 + + Returns: + bool: 긴 경로 지원이 활성화되어 있으면 True + """ + if not IS_WINDOWS: + return True + + # 현재 설정 확인 + success, stdout, _ = run_git_command(['config', '--global', 'core.longpaths'], check=False) + if success and stdout.strip().lower() == 'true': + return True + + # 긴 경로 지원 활성화 + print_info("Windows 긴 경로 지원을 활성화합니다...") + success, _, stderr = run_git_command(['config', '--global', 'core.longpaths', 'true'], check=False) + if success: + print_success("긴 경로 지원이 활성화되었습니다.") + return True + else: + print_warning(f"긴 경로 지원 활성화 실패: {stderr}") + print_warning("수동으로 실행하세요: git config --global core.longpaths true") + return False + + +def is_git_repository() -> bool: + """현재 디렉토리가 Git 저장소인지 확인""" + success, _, _ = run_git_command(['rev-parse', '--git-dir'], check=False) + return success + + +def get_git_root() -> Optional[Path]: + """Git 저장소 루트 경로 반환""" + success, stdout, _ = run_git_command(['rev-parse', '--show-toplevel'], check=False) + if success and stdout: + return Path(stdout) + return None + + +def get_current_branch() -> Optional[str]: + """현재 체크아웃된 브랜치명 반환""" + success, stdout, _ = run_git_command(['branch', '--show-current'], check=False) + if success and stdout: + return stdout + return None + + +def branch_exists(branch_name: str) -> bool: + """ + 브랜치 존재 여부 확인 + + Args: + branch_name: 확인할 브랜치명 + + Returns: + bool: 브랜치가 존재하면 True + """ + success, stdout, _ = run_git_command(['branch', '--list', branch_name], check=False) + if success and stdout: + # 출력 형식: " branch_name" 또는 "* branch_name" + branches = [line.strip().lstrip('* ') for line in stdout.split('\n')] + return branch_name in branches + return False + + +def create_branch(branch_name: str) -> bool: + """ + 현재 브랜치에서 새 브랜치 생성 + + Args: + branch_name: 생성할 브랜치명 + + Returns: + bool: 성공 여부 + """ + success, _, stderr = run_git_command(['branch', branch_name], check=False) + if not success: + print_error(f"브랜치 생성 실패: {stderr}") + return success + + +def get_worktree_list() -> Dict[str, str]: + """ + 현재 등록된 worktree 목록 반환 + + Returns: + Dict[str, str]: {worktree_path: branch_name} + """ + success, stdout, _ = run_git_command(['worktree', 'list', '--porcelain'], check=False) + if not success: + return {} + + worktrees = {} + current_path = None + + for line in stdout.split('\n'): + if line.startswith('worktree '): + current_path = line.replace('worktree ', '') + elif line.startswith('branch '): + branch = line.replace('branch ', '').replace('refs/heads/', '') + if current_path: + worktrees[current_path] = branch + current_path = None + + return worktrees + + +def prune_worktrees() -> bool: + """ + 유효하지 않은 worktree 정리 (git worktree prune) + + Returns: + bool: 성공 여부 + """ + success, _, stderr = run_git_command(['worktree', 'prune'], check=False) + if not success: + print_warning(f"Worktree prune 실패: {stderr}") + return success + + +def is_worktree_exists(worktree_path: Path) -> bool: + """ + 특정 경로에 worktree가 이미 존재하는지 확인 + + Git worktree 목록과 실제 디렉토리 존재 여부를 모두 확인합니다. + prunable 상태의 worktree는 자동으로 정리합니다. + + Args: + worktree_path: 확인할 worktree 경로 + + Returns: + bool: worktree가 유효하게 존재하면 True + """ + # 먼저 prunable worktree 정리 + prune_worktrees() + + worktrees = get_worktree_list() + worktree_path_resolved = worktree_path.resolve() + + for path in worktrees.keys(): + if Path(path).resolve() == worktree_path_resolved: + # Git 목록에 있으면 실제 디렉토리도 존재하는지 확인 + if Path(path).exists(): + return True + else: + # 디렉토리가 없으면 다시 prune 실행 + print_warning(f"Worktree 경로가 존재하지 않아 정리합니다: {path}") + prune_worktrees() + return False + + # 디렉토리만 존재하고 Git에 등록되지 않은 경우도 확인 + if worktree_path_resolved.exists(): + # .git 파일이 있는지 확인 (worktree의 특징) + git_file = worktree_path_resolved / '.git' + if git_file.exists(): + print_warning(f"디렉토리가 존재하지만 Git에 등록되지 않음: {worktree_path}") + return True + + return False + + +def create_worktree(branch_name: str, worktree_path: Path) -> Dict: + """ + Git worktree 생성 + + Args: + branch_name: 체크아웃할 브랜치명 + worktree_path: worktree를 생성할 경로 + + Returns: + Dict: { + 'success': bool, + 'path': str, + 'message': str, + 'is_existing': bool + } + """ + # 이미 존재하는지 확인 + if is_worktree_exists(worktree_path): + return { + 'success': True, + 'path': str(worktree_path.resolve()), + 'message': 'Worktree가 이미 존재합니다.', + 'is_existing': True + } + + # worktree 생성 + success, stdout, stderr = run_git_command( + ['worktree', 'add', str(worktree_path), branch_name], + check=False + ) + + if success: + return { + 'success': True, + 'path': str(worktree_path.resolve()), + 'message': 'Worktree 생성 완료!', + 'is_existing': False + } + else: + return { + 'success': False, + 'path': str(worktree_path.resolve()), + 'message': f'Worktree 생성 실패: {stderr}', + 'is_existing': False + } + + +# =================================================================== +# 경로 관련 함수 +# =================================================================== + +def normalize_branch_name(branch_name: str) -> str: + """ + 브랜치명을 폴더명으로 안전하게 변환 + + 특수문자 (#, /, \\\\, :, *, ?, ", <, >, |)를 _ 로 변환하고, + 연속된 _를 하나로 통합하며, 앞뒤 _를 제거합니다. + + Args: + branch_name: 원본 브랜치명 + + Returns: + str: 정규화된 폴더명 + + Example: + >>> normalize_branch_name("20260120_#163_Github_Projects") + "20260120_163_Github_Projects" + """ + # 특수문자를 _ 로 변환 + normalized = re.sub(SPECIAL_CHARS_PATTERN, '_', branch_name) + + # 연속된 _를 하나로 통합 + normalized = re.sub(r'_+', '_', normalized) + + # 앞뒤 _를 제거 + normalized = normalized.strip('_') + + return normalized + + +def get_worktree_root() -> Path: + """ + Worktree 루트 경로 계산 + + 현재 Git 저장소의 부모 디렉토리에 {프로젝트명}-Worktree 폴더 생성 + + Returns: + Path: Worktree 루트 경로 + + Example: + 현재: /Users/.../project/RomRom-FE + 반환: /Users/.../project/RomRom-FE-Worktree + """ + git_root = get_git_root() + if not git_root: + raise RuntimeError("Git 저장소 루트를 찾을 수 없습니다.") + + # 현재 Git 저장소의 이름 추출 (예: RomRom-FE) + project_name = git_root.name + + # 부모 디렉토리에 {프로젝트명}-Worktree 폴더 생성 + worktree_root_name = f"{project_name}-Worktree" + worktree_root = git_root.parent / worktree_root_name + + return worktree_root + + +def get_worktree_path(branch_name: str) -> Path: + """ + 특정 브랜치의 worktree 전체 경로 반환 + + Args: + branch_name: 브랜치명 (정규화 전) + + Returns: + Path: Worktree 경로 + + Example: + >>> get_worktree_path("20260120_#163_Github_Projects") + Path("/Users/.../project/RomRom-FE-Worktree/20260120_163_Github_Projects") + """ + worktree_root = get_worktree_root() + folder_name = normalize_branch_name(branch_name) + return worktree_root / folder_name + + +def ensure_directory(path: Path) -> bool: + """ + 디렉토리가 존재하지 않으면 생성 + + Args: + path: 생성할 디렉토리 경로 + + Returns: + bool: 성공 여부 + """ + try: + path.mkdir(parents=True, exist_ok=True) + return True + except Exception as e: + print_error(f"디렉토리 생성 실패: {e}") + return False + + +# =================================================================== +# 메인 워크플로우 +# =================================================================== + +def main() -> int: + """ + 메인 워크플로우 + + Returns: + int: Exit code (0: 성공, 1: 실패) + """ + print_header() + + # 1. 브랜치명 받기 (Windows 환경 대응) + branch_name = get_branch_name() + + if not branch_name: + print_error("브랜치명이 제공되지 않았습니다.") + print() + print("사용법:") + if IS_WINDOWS: + print(" Windows 환경:") + print(" 방법 1: 환경 변수 사용") + print(f' $env:GIT_BRANCH_NAME = "브랜치명"') + print(f" python -X utf8 {sys.argv[0]}") + print() + print(" 방법 2: 인자로 전달") + print(f' python -X utf8 {sys.argv[0]} "브랜치명"') + else: + print(f" python {sys.argv[0]} ") + print() + print("예시:") + print(f' python {sys.argv[0]} "20260120_#163_Github_Projects_에_대한_템플릿_개발_필요"') + return 1 + + print_step("📋", f"입력된 브랜치: {branch_name}") + + # 2. Git 저장소 확인 + if not is_git_repository(): + print_error("현재 디렉토리가 Git 저장소가 아닙니다.") + return 1 + + # 2-1. Windows 긴 경로 지원 확인 및 활성화 + if IS_WINDOWS: + check_and_enable_longpaths() + print() + + # 3. 브랜치명 정규화 + folder_name = normalize_branch_name(branch_name) + print_step("📁", f"폴더명: {folder_name}") + print() + + # 4. 브랜치 존재 확인 + print_step("🔍", "브랜치 확인 중...") + + if not branch_exists(branch_name): + print_warning("브랜치가 존재하지 않습니다.") + + current_branch = get_current_branch() + if current_branch: + print_step("🔄", f"현재 브랜치({current_branch})에서 새 브랜치 생성 중...") + else: + print_step("🔄", "새 브랜치 생성 중...") + + if not create_branch(branch_name): + print_error("브랜치 생성에 실패했습니다.") + return 1 + + print_success("브랜치 생성 완료!") + else: + print_success("브랜치가 이미 존재합니다.") + + print() + + # 5. Worktree 경로 계산 + try: + worktree_path = get_worktree_path(branch_name) + except RuntimeError as e: + print_error(str(e)) + return 1 + + print_step("📂", f"Worktree 경로: {worktree_path}") + print() + + # 6. Worktree 존재 확인 + print_step("🔍", "Worktree 확인 중...") + + if is_worktree_exists(worktree_path): + print_info("Worktree가 이미 존재합니다.") + print() + print_step("📍", f"경로: {worktree_path.resolve()}") + return 0 + + # 7. Worktree 루트 디렉토리 생성 + worktree_root = get_worktree_root() + if not ensure_directory(worktree_root): + return 1 + + # 8. Worktree 생성 + print_step("🔄", "Worktree 생성 중...") + + result = create_worktree(branch_name, worktree_path) + + if result['success']: + if result['is_existing']: + print_info(result['message']) + else: + print_success(result['message']) + + print() + print_step("📍", f"경로: {result['path']}") + return 0 + else: + print_error(result['message']) + return 1 + + +# =================================================================== +# 엔트리 포인트 +# =================================================================== + +if __name__ == "__main__": + try: + exit_code = main() + sys.exit(exit_code) + except KeyboardInterrupt: + print() + print_warning("사용자에 의해 중단되었습니다.") + sys.exit(130) + except Exception as e: + print() + print_error(f"예상치 못한 오류가 발생했습니다: {e}") + sys.exit(1) From 7aadda777f392cc93b3e41215c6d25ea02131ec9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 27 Jan 2026 06:35:32 +0000 Subject: [PATCH 6/8] =?UTF-8?q?MapSy-FE=20=EB=B2=84=EC=A0=84=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=201.0.18=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- version.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 30659da..ea5575d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mapsy description: "MapSy - Flutter Application" publish_to: "none" -version: 1.0.17+17 +version: 1.0.18+18 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index fc45edc..8bb0cb4 100644 --- a/version.yml +++ b/version.yml @@ -34,11 +34,11 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.0.17" -version_code: 18 # app build number +version: "1.0.18" +version_code: 19 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-01-20 06:37:03" + last_updated: "2026-01-27 06:35:32" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE" From 924faf061526412ee93471a30d3f336a824781c1 Mon Sep 17 00:00:00 2001 From: SUH SAECHAN Date: Tue, 27 Jan 2026 15:35:59 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20Spring=20init=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20workflow=20=EA=B5=AC=ED=98=84=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=20:=20feat=20:=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20custom?= =?UTF-8?q?=20command=20=EC=B6=94=EA=B0=803=20https://github.com/MapSee-La?= =?UTF-8?q?b/MapSee-BE/issues/2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index ac2e729..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(firebase --version:*)", - "Bash(find:*)" - ] - } -} From 8c4a9f2df3ed77ef30bc7b005c605b624021d785 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 27 Jan 2026 06:36:04 +0000 Subject: [PATCH 8/8] =?UTF-8?q?MapSy-FE=20=EB=B2=84=EC=A0=84=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B4=80=EB=A6=AC:=20chore:=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=201.0.19=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- version.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index ea5575d..45b1397 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: mapsy description: "MapSy - Flutter Application" publish_to: "none" -version: 1.0.18+18 +version: 1.0.19+19 environment: sdk: ^3.9.2 dependencies: diff --git a/version.yml b/version.yml index 8bb0cb4..b285483 100644 --- a/version.yml +++ b/version.yml @@ -34,11 +34,11 @@ # - 버전은 항상 높은 버전으로 자동 동기화됩니다 # =================================================================== -version: "1.0.18" -version_code: 19 # app build number +version: "1.0.19" +version_code: 20 # app build number project_type: "flutter" # spring, flutter, react, react-native, react-native-expo, node, python, basic metadata: - last_updated: "2026-01-27 06:35:32" + last_updated: "2026-01-27 06:36:04" last_updated_by: "Cassiiopeia" default_branch: "main" integrated_from: "SUH-DEVOPS-TEMPLATE"