diff --git a/internal/policy/manager.go b/internal/policy/manager.go index 0515f66..52777c8 100644 --- a/internal/policy/manager.go +++ b/internal/policy/manager.go @@ -92,18 +92,18 @@ func ValidatePolicy(policy *schema.UserPolicy) error { } // Validate rules - ruleNumbers := make(map[int]bool) + ruleIDs := make(map[string]bool) for i, rule := range policy.Rules { if rule.Say == "" { return fmt.Errorf("rule %d: 'say' field is required", i+1) } - if rule.No <= 0 { - return fmt.Errorf("rule %d: 'no' field must be positive", i+1) + if rule.ID == "" { + return fmt.Errorf("rule %d: 'id' field is required", i+1) } - if ruleNumbers[rule.No] { - return fmt.Errorf("duplicate rule number: %d", rule.No) + if ruleIDs[rule.ID] { + return fmt.Errorf("duplicate rule id: %s", rule.ID) } - ruleNumbers[rule.No] = true + ruleIDs[rule.ID] = true } // Validate RBAC roles diff --git a/internal/policy/templates/go-template.json b/internal/policy/templates/go-template.json index 4c01723..884c81f 100644 --- a/internal/policy/templates/go-template.json +++ b/internal/policy/templates/go-template.json @@ -15,54 +15,54 @@ }, "rules": [ { - "no": 1, + "id": "1", "say": "패키지 이름은 소문자 한 단어로 작성합니다 (예: http, json, user)", "category": "naming", "example": "// ✅ 좋은 예:\npackage user\npackage database\npackage httputil\n\n// ❌ 나쁜 예:\npackage UserManagement\npackage data_base\npackage HTTP_Util" }, { - "no": 2, + "id": "2", "say": "변수와 함수 이름은 camelCase 또는 PascalCase를 사용합니다 (public은 대문자 시작)", "category": "naming" }, { - "no": 3, + "id": "3", "say": "에러는 항상 처리하고, 무시하는 경우 명시적으로 _ 를 사용합니다", "category": "error_handling", "example": "// ✅ 좋은 예:\nfile, err := os.Open(\"config.json\")\nif err != nil {\n return fmt.Errorf(\"failed to open config: %w\", err)\n}\ndefer file.Close()\n\n// ❌ 나쁜 예:\nfile, _ := os.Open(\"config.json\")\ndefer file.Close()" }, { - "no": 4, + "id": "4", "say": "함수는 에러를 마지막 반환값으로 반환합니다", "category": "error_handling" }, { - "no": 5, + "id": "5", "say": "인터페이스 이름은 -er 접미사를 사용합니다 (예: Reader, Writer, Handler)", "category": "naming" }, { - "no": 6, + "id": "6", "say": "gofmt로 코드를 포맷팅합니다", "category": "formatting" }, { - "no": 7, + "id": "7", "say": "공개(exported) 함수와 타입에는 주석을 작성합니다", "category": "documentation" }, { - "no": 8, + "id": "8", "say": "context.Context를 함수의 첫 번째 파라미터로 전달합니다", "category": "error_handling" }, { - "no": 9, + "id": "9", "say": "goroutine 사용 시 적절한 동기화(sync, channel)를 사용합니다", "category": "error_handling" }, { - "no": 10, + "id": "10", "say": "defer를 사용하여 리소스 정리를 보장합니다", "category": "error_handling" } diff --git a/internal/policy/templates/node-template.json b/internal/policy/templates/node-template.json index d8ef657..43166ca 100644 --- a/internal/policy/templates/node-template.json +++ b/internal/policy/templates/node-template.json @@ -19,55 +19,55 @@ }, "rules": [ { - "no": 1, + "id": "1", "say": "환경 변수는 process.env가 아닌 설정 파일(config.js)을 통해 접근합니다", "category": "security" }, { - "no": 2, + "id": "2", "say": "민감한 정보(API 키, 비밀번호)는 절대 하드코딩하지 않습니다", "category": "security", "example": "// ✅ 좋은 예:\nconst apiKey = process.env.API_KEY;\nconst dbPassword = config.get('database.password');\n\n// ❌ 나쁜 예:\nconst apiKey = 'sk-1234567890abcdef';\nconst dbPassword = 'mySecretPassword123';" }, { - "no": 3, + "id": "3", "say": "모든 비동기 함수는 try-catch 블록으로 에러를 처리합니다", "category": "error_handling", "example": "// ✅ 좋은 예:\nasync function fetchUser(id) {\n try {\n const user = await db.users.findById(id);\n return user;\n } catch (error) {\n logger.error('Failed to fetch user:', error);\n throw new ApiError('User not found', 404);\n }\n}\n\n// ❌ 나쁜 예:\nasync function fetchUser(id) {\n const user = await db.users.findById(id);\n return user;\n}" }, { - "no": 4, + "id": "4", "say": "API 라우트 핸들러는 항상 적절한 HTTP 상태 코드를 반환합니다", "category": "error_handling" }, { - "no": 5, + "id": "5", "say": "데이터베이스 쿼리는 SQL injection을 방지하기 위해 parameterized query를 사용합니다", "category": "security", "example": "// ✅ 좋은 예:\nconst result = await db.query('SELECT * FROM users WHERE id = ?', [userId]);\n\n// ❌ 나쁜 예:\nconst result = await db.query(`SELECT * FROM users WHERE id = ${userId}`);" }, { - "no": 6, + "id": "6", "say": "파일 이름은 kebab-case를 사용합니다 (예: user-controller.js, auth-service.js)", "category": "naming" }, { - "no": 7, + "id": "7", "say": "클래스 이름은 PascalCase, 함수/변수는 camelCase를 사용합니다", "category": "naming" }, { - "no": 8, + "id": "8", "say": "미들웨어 함수는 next()를 명시적으로 호출하거나 응답을 보냅니다", "category": "error_handling" }, { - "no": 9, + "id": "9", "say": "큰 의존성 패키지는 필요한 부분만 import합니다 (tree-shaking)", "category": "performance" }, { - "no": 10, + "id": "10", "say": "로그 메시지는 적절한 레벨(debug, info, warn, error)을 사용합니다", "category": "documentation" } diff --git a/internal/policy/templates/python-template.json b/internal/policy/templates/python-template.json index b89596f..af94337 100644 --- a/internal/policy/templates/python-template.json +++ b/internal/policy/templates/python-template.json @@ -19,55 +19,55 @@ }, "rules": [ { - "no": 1, + "id": "1", "say": "PEP 8 스타일 가이드를 준수합니다 (들여쓰기 4칸, 최대 라인 길이 79자)", "category": "formatting" }, { - "no": 2, + "id": "2", "say": "함수와 변수 이름은 snake_case를 사용합니다 (예: calculate_total, user_name)", "category": "naming", "example": "# ✅ 좋은 예:\ndef calculate_total_price(items):\n total_price = sum(item.price for item in items)\n return total_price\n\n# ❌ 나쁜 예:\ndef calculateTotalPrice(items):\n totalPrice = sum(item.price for item in items)\n return totalPrice" }, { - "no": 3, + "id": "3", "say": "클래스 이름은 PascalCase를 사용합니다 (예: UserModel, DataProcessor)", "category": "naming", "example": "# ✅ 좋은 예:\nclass UserProfile:\n def __init__(self, name):\n self.name = name\n\n# ❌ 나쁜 예:\nclass user_profile:\n def __init__(self, name):\n self.name = name" }, { - "no": 4, + "id": "4", "say": "상수는 대문자와 언더스코어를 사용합니다 (예: MAX_SIZE, API_KEY)", "category": "naming" }, { - "no": 5, + "id": "5", "say": "모든 함수와 클래스는 docstring을 작성합니다", "category": "documentation" }, { - "no": 6, + "id": "6", "say": "타입 힌트를 사용하여 함수 시그니처를 명확히 합니다", "category": "documentation" }, { - "no": 7, + "id": "7", "say": "예외는 구체적인 타입으로 처리합니다 (Exception보다는 ValueError, TypeError 등)", "category": "error_handling", "example": "# ✅ 좋은 예:\ntry:\n result = int(user_input)\nexcept ValueError as e:\n logger.error(f\"Invalid input: {e}\")\n result = 0\n\n# ❌ 나쁜 예:\ntry:\n result = int(user_input)\nexcept:\n result = 0" }, { - "no": 8, + "id": "8", "say": "리소스(파일, 데이터베이스)는 with 문으로 관리합니다", "category": "error_handling" }, { - "no": 9, + "id": "9", "say": "민감한 정보는 환경 변수나 설정 파일을 통해 관리합니다", "category": "security" }, { - "no": 10, + "id": "10", "say": "리스트 컴프리헨션은 3줄 이내로 유지하고, 복잡한 경우 일반 for문을 사용합니다", "category": "formatting" } diff --git a/internal/policy/templates/react-template.json b/internal/policy/templates/react-template.json index 3c16a26..82d0d6b 100644 --- a/internal/policy/templates/react-template.json +++ b/internal/policy/templates/react-template.json @@ -19,60 +19,60 @@ }, "rules": [ { - "no": 1, + "id": "1", "say": "컴포넌트 이름은 PascalCase를 사용합니다 (예: UserProfile, NavBar)", "category": "naming", "languages": ["javascript", "typescript", "jsx", "tsx"], "example": "// ✅ 좋은 예:\nfunction UserProfile() { ... }\nfunction NavBar() { ... }\n\n// ❌ 나쁜 예:\nfunction userProfile() { ... }\nfunction nav_bar() { ... }" }, { - "no": 2, + "id": "2", "say": "React Hooks는 함수 컴포넌트 최상단에서만 호출합니다", "category": "error_handling", "languages": ["javascript", "typescript", "jsx", "tsx"], "example": "// ✅ 좋은 예:\nfunction MyComponent() {\n const [count, setCount] = useState(0);\n const value = useMemo(() => count * 2, [count]);\n return
{value}
;\n}\n\n// ❌ 나쁜 예:\nfunction MyComponent() {\n if (condition) {\n const [count, setCount] = useState(0); // 조건부 호출 금지\n }\n}" }, { - "no": 3, + "id": "3", "say": "useEffect의 의존성 배열을 정확히 명시하여 무한 루프를 방지합니다", "category": "error_handling", "languages": ["javascript", "typescript"] }, { - "no": 4, + "id": "4", "say": "Props는 구조 분해 할당으로 받습니다 (예: function Button({ label, onClick }) {...})", "category": "formatting", "languages": ["javascript", "typescript", "jsx", "tsx"], "example": "// ✅ 좋은 예:\nfunction Button({ label, onClick, disabled = false }) {\n return ;\n}\n\n// ❌ 나쁜 예:\nfunction Button(props) {\n return ;\n}" }, { - "no": 5, + "id": "5", "say": "컴포넌트 파일 하나당 하나의 컴포넌트만 export합니다", "category": "formatting" }, { - "no": 6, + "id": "6", "say": "이벤트 핸들러 함수는 'handle' 접두사를 사용합니다 (예: handleClick, handleSubmit)", "category": "naming" }, { - "no": 7, + "id": "7", "say": "boolean 타입의 Props는 'is', 'has', 'should' 접두사를 사용합니다 (예: isOpen, hasError)", "category": "naming" }, { - "no": 8, + "id": "8", "say": "인라인 스타일 대신 CSS 모듈 또는 styled-components를 사용합니다", "category": "formatting" }, { - "no": 9, + "id": "9", "say": "Key prop에 배열 인덱스를 사용하지 않습니다. 고유한 ID를 사용하세요", "category": "performance", "example": "// ✅ 좋은 예:\n{users.map(user => )}\n\n// ❌ 나쁜 예:\n{users.map((user, index) => )}" }, { - "no": 10, + "id": "10", "say": "큰 리스트는 React.memo 또는 useMemo로 최적화합니다", "category": "performance" } diff --git a/internal/policy/templates/typescript-template.json b/internal/policy/templates/typescript-template.json index cdf6b2d..f460f19 100644 --- a/internal/policy/templates/typescript-template.json +++ b/internal/policy/templates/typescript-template.json @@ -15,54 +15,54 @@ }, "rules": [ { - "no": 1, + "id": "1", "say": "any 타입 사용을 최소화하고 구체적인 타입을 정의합니다", "category": "error_handling", "example": "// ✅ 좋은 예:\ninterface User {\n id: number;\n name: string;\n}\nfunction processUser(user: User): void { ... }\n\n// ❌ 나쁜 예:\nfunction processUser(user: any): void { ... }" }, { - "no": 2, + "id": "2", "say": "인터페이스와 타입 별칭의 이름은 PascalCase를 사용합니다", "category": "naming" }, { - "no": 3, + "id": "3", "say": "타입 정의 파일(.d.ts)은 types/ 폴더에 위치시킵니다", "category": "formatting" }, { - "no": 4, + "id": "4", "say": "제네릭 타입 파라미터는 의미 있는 이름을 사용합니다 (T보다는 TItem, TResponse)", "category": "naming" }, { - "no": 5, + "id": "5", "say": "유틸리티 타입(Partial, Pick, Omit 등)을 적극 활용합니다", "category": "performance" }, { - "no": 6, + "id": "6", "say": "strict 모드를 활성화하여 엄격한 타입 체크를 수행합니다", "category": "error_handling" }, { - "no": 7, + "id": "7", "say": "열거형(enum) 대신 const assertion이나 union 타입을 고려합니다", "category": "performance" }, { - "no": 8, + "id": "8", "say": "함수 시그니처에 반환 타입을 명시적으로 선언합니다", "category": "documentation", "example": "// ✅ 좋은 예:\nfunction calculateTotal(price: number, quantity: number): number {\n return price * quantity;\n}\n\n// ❌ 나쁜 예:\nfunction calculateTotal(price: number, quantity: number) {\n return price * quantity;\n}" }, { - "no": 9, + "id": "9", "say": "readonly를 사용하여 불변성을 보장합니다", "category": "error_handling" }, { - "no": 10, + "id": "10", "say": "타입 가드를 사용하여 런타임 타입 안전성을 확보합니다", "category": "error_handling" } diff --git a/internal/policy/templates/vue-template.json b/internal/policy/templates/vue-template.json index 820a535..2dd1f9d 100644 --- a/internal/policy/templates/vue-template.json +++ b/internal/policy/templates/vue-template.json @@ -15,54 +15,54 @@ }, "rules": [ { - "no": 1, + "id": "1", "say": "컴포넌트 이름은 PascalCase를 사용합니다 (예: UserProfile.vue)", "category": "naming", "example": "\n\n\n\n" }, { - "no": 2, + "id": "2", "say": "Composition API를 사용할 때 setup() 함수 또는 \n\n\n" }, { - "no": 3, + "id": "3", "say": "Props는 타입과 기본값을 명시합니다", "category": "documentation" }, { - "no": 4, + "id": "4", "say": "이벤트 이름은 kebab-case를 사용합니다 (예: @user-updated, @item-deleted)", "category": "naming" }, { - "no": 5, + "id": "5", "say": "v-for에는 항상 :key를 지정합니다", "category": "error_handling" }, { - "no": 6, + "id": "6", "say": "computed는 부수 효과 없이 순수 함수로 작성합니다", "category": "performance" }, { - "no": 7, + "id": "7", "say": "composables 함수 이름은 'use' 접두사를 사용합니다 (예: useUser, useFetch)", "category": "naming" }, { - "no": 8, + "id": "8", "say": "템플릿 내 복잡한 표현식은 computed 속성으로 분리합니다", "category": "formatting" }, { - "no": 9, + "id": "9", "say": "글로벌 상태는 Pinia 또는 Vuex를 사용하여 관리합니다", "category": "formatting" }, { - "no": 10, + "id": "10", "say": "컴포넌트는 단일 책임 원칙을 따르며 200줄 이하로 유지합니다", "category": "formatting" } diff --git a/internal/server/static/policy-editor.js b/internal/server/static/policy-editor.js index 07671f5..5794c8b 100644 --- a/internal/server/static/policy-editor.js +++ b/internal/server/static/policy-editor.js @@ -381,27 +381,27 @@ function renderRules() { } function createRuleElement(rule, index) { - const actualIndex = appState.policy.rules.findIndex(r => r.no === rule.no); + const actualIndex = appState.policy.rules.findIndex(r => r.id === rule.id); return ` -
+
- ${rule.no}. + ${rule.id}. ${rule.say || '새 규칙 (내용을 입력하세요)'}
- +
- +
- ${Object.keys(CATEGORY_COLORS).filter(c => c !== 'default').map(cat => ` @@ -410,12 +410,12 @@ function createRuleElement(rule, index) {
- +
- +
@@ -424,11 +424,11 @@ function createRuleElement(rule, index) { } function handleRuleUpdate(e) { - const ruleNo = parseInt(e.target.dataset.ruleNo); - const rule = appState.policy.rules.find(r => r.no === ruleNo); + const ruleId = e.target.dataset.ruleId; + const rule = appState.policy.rules.find(r => r.id === ruleId); if (!rule) return; - const ruleElement = document.querySelector(`.rule-details[data-rule-no="${ruleNo}"]`); + const ruleElement = document.querySelector(`.rule-details[data-rule-id="${ruleId}"]`); if (e.target.classList.contains('say-input')) { rule.say = e.target.value.trim(); @@ -448,15 +448,15 @@ function handleRuleUpdate(e) { function handleDeleteRule(e) { e.preventDefault(); - const ruleNo = parseInt(e.target.dataset.ruleNo); + const ruleId = e.target.dataset.ruleId; if (!confirm('이 규칙을 정말 삭제하시겠습니까?')) return; - appState.policy.rules = appState.policy.rules.filter(r => r.no !== ruleNo); + appState.policy.rules = appState.policy.rules.filter(r => r.id !== ruleId); // Renumber rules appState.policy.rules.forEach((rule, index) => { - rule.no = index + 1; + rule.id = String(index + 1); }); renderRules(); @@ -465,14 +465,14 @@ function handleDeleteRule(e) { } function handleAddRule() { - const newNo = appState.policy.rules.length + 1; - const newRule = { no: newNo, say: '', category: '', languages: [], example: '' }; + const newId = String(appState.policy.rules.length + 1); + const newRule = { id: newId, say: '', category: '', languages: [], example: '' }; appState.policy.rules.push(newRule); renderRules(); // Open the new rule details setTimeout(() => { - const newRuleElement = document.querySelector(`.rule-details[data-rule-no="${newNo}"]`); + const newRuleElement = document.querySelector(`.rule-details[data-rule-id="${newId}"]`); if (newRuleElement) { newRuleElement.setAttribute('open', ''); newRuleElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); diff --git a/pkg/schema/types.go b/pkg/schema/types.go index e8f5c5e..a3bba37 100644 --- a/pkg/schema/types.go +++ b/pkg/schema/types.go @@ -33,8 +33,7 @@ type UserDefaults struct { // UserRule represents a single rule in user schema type UserRule struct { - No int `json:"no,omitempty"` // symphonyclient integration: rule number for ordering - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` // symphonyclient integration: rule number for ordering Say string `json:"say"` Category string `json:"category,omitempty"` Languages []string `json:"languages,omitempty"`