# 코드 컨벤션 문서 ## 1. 개요 이 문서는 "오늘 뭐 먹Z?" 프로젝트의 코드 작성 규칙과 스타일 가이드를 정의합니다. 일관된 코드 스타일은 가독성을 높이고 유지보수를 용이하게 합니다. ## 2. 일반 원칙 ### 2.1 기본 규칙 - **DRY (Don't Repeat Yourself)**: 코드 중복 최소화 - **KISS (Keep It Simple, Stupid)**: 단순하고 명확한 코드 작성 - **YAGNI (You Aren't Gonna Need It)**: 필요하지 않은 기능 미리 구현 금지 - **단일 책임 원칙**: 하나의 클래스/함수는 하나의 책임만 ### 2.2 언어별 규칙 - **코드**: 영어 (변수명, 함수명, 클래스명) - **주석**: 한국어 - **커밋 메시지**: 한국어 - **문서**: 한국어 ## 3. 네이밍 컨벤션 ### 3.1 기본 네이밍 규칙 | 요소 | 스타일 | 예시 | |------|--------|------| | 클래스 | PascalCase | `NaverUrlProcessor`, `RestaurantRepository` | | 인터페이스 | PascalCase | `IRestaurantRepository` (선택적 I 접두사) | | 함수/메서드 | camelCase | `processNaverUrl`, `searchRestaurants` | | 변수 | camelCase | `restaurantName`, `maxDistance` | | 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT`, `API_TIMEOUT` | | 파일명 | snake_case | `naver_url_processor.dart` | | 폴더명 | snake_case | `data_sources`, `use_cases` | ### 3.2 의미있는 이름 작성 ```dart // ❌ 나쁜 예 String n = "김밥천국"; int d = 500; List r = []; // ✅ 좋은 예 String restaurantName = "김밥천국"; int maxDistanceInMeters = 500; List nearbyRestaurants = []; ``` ### 3.3 Boolean 변수 네이밍 ```dart // ❌ 나쁜 예 bool loading = true; bool error = false; // ✅ 좋은 예 bool isLoading = true; bool hasError = false; bool canDelete = true; bool shouldRefresh = false; ``` ### 3.4 함수/메서드 네이밍 ```dart // 동사로 시작 Future saveRestaurant(Restaurant restaurant); Future> fetchNearbyRestaurants(); bool validateUrl(String url); void clearCache(); // 상태 확인 메서드는 is/has/can으로 시작 bool isValidNaverUrl(String url); bool hasLocationPermission(); bool canProcessUrl(String url); ``` ## 4. 파일 구조 및 조직 ### 4.1 파일 구조 템플릿 ```dart // 1. 라이브러리 선언 (필요한 경우) library restaurant_repository; // 2. Dart 임포트 import 'dart:async'; import 'dart:convert'; // 3. 패키지 임포트 (알파벳 순) import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // 4. 프로젝트 임포트 (알파벳 순) import 'package:lunchpick/core/constants/api_keys.dart'; import 'package:lunchpick/domain/entities/restaurant.dart'; // 5. Part 파일 (있는 경우) part 'restaurant_repository.g.dart'; // 6. 전역 상수 const int kDefaultTimeout = 30; // 7. 클래스 정의 class RestaurantRepository { // 구현 } ``` ### 4.2 클래스 내부 구조 ```dart class NaverUrlProcessor { // 1. 정적 상수 static const int maxRetryCount = 3; // 2. 정적 변수 static String? _cachedUrl; // 3. 인스턴스 변수 (private 먼저) final NaverMapParser _mapParser; final NaverLocalApiClient _apiClient; late final Logger _logger; // 4. 생성자 NaverUrlProcessor({ required NaverMapParser mapParser, required NaverLocalApiClient apiClient, }) : _mapParser = mapParser, _apiClient = apiClient { _logger = Logger('NaverUrlProcessor'); } // 5. 팩토리 생성자 factory NaverUrlProcessor.create() { return NaverUrlProcessor( mapParser: NaverMapParser(), apiClient: NaverLocalApiClient(), ); } // 6. getter/setter bool get isReady => _mapParser != null && _apiClient != null; // 7. public 메서드 Future processUrl(String url) async { // 구현 } // 8. private 메서드 Future _resolveUrl(String url) async { // 구현 } // 9. 정적 메서드 static bool isValidUrl(String url) { // 구현 } } ``` ## 5. 코딩 스타일 ### 5.1 들여쓰기 및 정렬 ```dart // 2 스페이스 들여쓰기 사용 class Restaurant { final String id; final String name; Restaurant({ required this.id, required this.name, }); } // 긴 파라미터는 세로 정렬 final restaurant = Restaurant( id: generateId(), name: '김밥천국', category: '분식', roadAddress: '서울특별시 강남구 테헤란로 123', latitude: 37.123456, longitude: 127.123456, ); ``` ### 5.2 줄 길이 - 최대 80자 권장 (하드 리밋: 120자) - 긴 문자열은 여러 줄로 분할 ```dart // ❌ 나쁜 예 String errorMessage = '네이버 지도 URL 파싱 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; // ✅ 좋은 예 String errorMessage = '네이버 지도 URL 파싱 중 오류가 발생했습니다. ' '잠시 후 다시 시도해주세요.'; ``` ### 5.3 중괄호 사용 ```dart // 한 줄이어도 중괄호 사용 if (isValid) { return true; } // 여러 조건은 읽기 쉽게 정렬 if (restaurant.name.isNotEmpty && restaurant.category.isNotEmpty && restaurant.latitude != 0) { // 처리 } ``` ### 5.4 빈 줄 사용 ```dart class RestaurantService { final RestaurantRepository _repository; final CacheManager _cache; RestaurantService(this._repository, this._cache); // 메서드 사이에 빈 줄 Future> getNearbyRestaurants() async { // 구현 } Future addRestaurant(Restaurant restaurant) async { // 구현 } } ``` ## 6. 주석 작성 규칙 ### 6.1 문서 주석 ```dart /// 네이버 단축 URL을 처리하여 식당 정보를 추출합니다. /// /// [url]은 네이버 지도 또는 naver.me 단축 URL이어야 합니다. /// /// 처리 과정: /// 1. URL 유효성 검증 /// 2. 단축 URL 리다이렉션 /// 3. HTML 스크래핑 /// 4. 네이버 로컬 API 검색 /// 5. 정보 병합 /// /// 실패 시 [NaverUrlException]을 던집니다. Future processNaverUrl(String url) async { // 구현 } ``` ### 6.2 인라인 주석 ```dart // 단축 URL인 경우 리다이렉션 처리 if (url.contains('naver.me')) { url = await _resolveShortUrl(url); } // TODO: 캐싱 로직 추가 필요 // FIXME: 타임아웃 처리 개선 필요 // HACK: CORS 우회를 위한 임시 방법 ``` ### 6.3 주석 작성 원칙 - **Why, not What**: 코드가 무엇을 하는지가 아닌 왜 그렇게 하는지 설명 - **한국어 사용**: 모든 주석은 한국어로 작성 - **최신 상태 유지**: 코드 변경 시 주석도 함께 업데이트 ## 7. 에러 처리 ### 7.1 예외 정의 ```dart // 계층별 예외 클래스 정의 abstract class AppException implements Exception { final String message; final String? code; final dynamic originalError; AppException(this.message, {this.code, this.originalError}); } class NetworkException extends AppException { NetworkException(String message, {String? code}) : super(message, code: code); } class ParseException extends AppException { ParseException(String message, {dynamic originalError}) : super(message, originalError: originalError); } ``` ### 7.2 에러 처리 패턴 ```dart Future fetchRestaurant(String id) async { try { final response = await _api.getRestaurant(id); return Restaurant.fromJson(response); } on DioException catch (e) { // 네트워크 에러 처리 _logger.error('네트워크 에러 발생', error: e); throw NetworkException('식당 정보를 가져올 수 없습니다'); } on FormatException catch (e) { // 파싱 에러 처리 _logger.error('데이터 파싱 실패', error: e); throw ParseException('잘못된 데이터 형식입니다'); } catch (e) { // 예상치 못한 에러 _logger.error('알 수 없는 에러', error: e); rethrow; } } ``` ## 8. 비동기 프로그래밍 ### 8.1 async/await 사용 ```dart // ✅ 좋은 예 Future processRestaurants() async { final restaurants = await fetchRestaurants(); for (final restaurant in restaurants) { await processRestaurant(restaurant); } } // ❌ 나쁜 예 (불필요한 then 체이닝) void processRestaurants() { fetchRestaurants().then((restaurants) { restaurants.forEach((restaurant) { processRestaurant(restaurant); }); }); } ``` ### 8.2 동시 실행 ```dart // 병렬 실행이 가능한 경우 Future initializeApp() async { final results = await Future.wait([ _loadUserSettings(), _fetchRestaurants(), _checkLocationPermission(), ]); } // 순차 실행이 필요한 경우 Future processInOrder() async { final user = await _loadUser(); final settings = await _loadUserSettings(user.id); final restaurants = await _fetchUserRestaurants(user.id); } ``` ## 9. 상태 관리 (Riverpod) ### 9.1 Provider 네이밍 ```dart // Provider 이름은 제공하는 값 + Provider final restaurantListProvider = FutureProvider>((ref) { return ref.watch(restaurantRepositoryProvider).getAllRestaurants(); }); // StateNotifierProvider는 notifier 추가 final restaurantFilterNotifierProvider = StateNotifierProvider((ref) { return RestaurantFilterNotifier(); }); ``` ### 9.2 Provider 구조화 ```dart // providers/restaurant_provider.dart final restaurantRepositoryProvider = Provider((ref) { return RestaurantRepositoryImpl(); }); final nearbyRestaurantsProvider = FutureProvider>((ref) { final location = ref.watch(locationProvider); final repository = ref.watch(restaurantRepositoryProvider); return repository.getNearbyRestaurants( latitude: location.latitude, longitude: location.longitude, radius: 500, ); }); ``` ## 10. 테스트 코드 작성 ### 10.1 테스트 파일 구조 ```dart // test/data/datasources/remote/naver_url_processor_test.dart void main() { // 테스트 대상 선언 late NaverUrlProcessor processor; late MockNaverMapParser mockMapParser; late MockNaverLocalApiClient mockApiClient; // 셋업 setUp(() { mockMapParser = MockNaverMapParser(); mockApiClient = MockNaverLocalApiClient(); processor = NaverUrlProcessor( mapParser: mockMapParser, apiClient: mockApiClient, ); }); // 테스트 그룹화 group('processNaverUrl', () { test('유효한 단축 URL을 처리해야 함', () async { // Given const url = 'https://naver.me/abc123'; when(() => mockMapParser.parseUrl(any())).thenAnswer( (_) async => testRestaurant, ); // When final result = await processor.processNaverUrl(url); // Then expect(result.name, equals('테스트 식당')); verify(() => mockMapParser.parseUrl(url)).called(1); }); test('유효하지 않은 URL은 예외를 던져야 함', () async { // Given const invalidUrl = 'https://google.com'; // When & Then expect( () => processor.processNaverUrl(invalidUrl), throwsA(isA()), ); }); }); } ``` ### 10.2 테스트 네이밍 ```dart // 테스트 이름은 한국어로 명확하게 test('레스토랑 이름이 비어있으면 예외를 던져야 함', () {}); test('중복된 레스토랑은 추가되지 않아야 함', () {}); test('거리 계산이 정확해야 함', () {}); ``` ## 11. 성능 최적화 규칙 ### 11.1 위젯 최적화 ```dart // const 생성자 사용 class RestaurantCard extends StatelessWidget { const RestaurantCard({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const Card( child: Padding( padding: EdgeInsets.all(16.0), child: Text('Restaurant'), ), ); } } // 무거운 빌드 메서드 분리 class RestaurantList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return ListView.builder( itemBuilder: (context, index) => RestaurantItem(index: index), ); } } ``` ### 11.2 메모리 관리 ```dart class RestaurantService { // 캐시 크기 제한 final _cache = LruMap(maximumSize: 100); // 리소스 정리 void dispose() { _cache.clear(); _subscription?.cancel(); } } ``` ## 12. Git 커밋 규칙 ### 12.1 커밋 메시지 형식 ``` 타입(범위): 제목 본문 (선택사항) 이슈: #123 ``` ### 12.2 커밋 타입 - `feat`: 새로운 기능 - `fix`: 버그 수정 - `refactor`: 리팩토링 - `style`: 코드 스타일 변경 - `test`: 테스트 추가/수정 - `docs`: 문서 수정 - `chore`: 빌드, 설정 변경 ### 12.3 커밋 예시 ``` feat(restaurant): 네이버 단축 URL 처리 기능 추가 - NaverUrlProcessor 클래스 구현 - 네이버 로컬 API 클라이언트 추가 - URL 매칭 알고리즘 구현 이슈: #42 ``` ## 13. 코드 리뷰 체크리스트 ### 13.1 기능 확인 - [ ] 요구사항을 모두 충족하는가? - [ ] 엣지 케이스를 처리하는가? - [ ] 에러 처리가 적절한가? ### 13.2 코드 품질 - [ ] 네이밍 컨벤션을 따르는가? - [ ] 주석이 적절히 작성되었는가? - [ ] 중복 코드가 없는가? - [ ] 함수/클래스가 단일 책임을 가지는가? ### 13.3 테스트 - [ ] 테스트가 작성되었는가? - [ ] 테스트 커버리지가 충분한가? - [ ] 테스트가 의미있는가? ### 13.4 성능 - [ ] 불필요한 리빌드가 없는가? - [ ] 메모리 누수 가능성이 없는가? - [ ] 비동기 처리가 적절한가? ## 14. 프로젝트별 특별 규칙 ### 14.1 네이버 관련 코드 - 모든 네이버 관련 클래스는 `Naver` 접두사 사용 - API 응답은 항상 null 체크 - 스크래핑 선택자는 상수로 정의 ### 14.2 Restaurant 엔티티 - 불변 객체로 유지 - copyWith 메서드 제공 - 좌표는 항상 유효성 검증 ### 14.3 로컬 저장소 - Hive 박스 이름은 상수로 정의 - 마이그레이션 전략 문서화 - 트랜잭션 단위로 처리