Files
lunchpick/doc/03_architecture/code_convention.md
JiWoong Sul 85fde36157 feat: 초기 프로젝트 설정 및 LunchPick 앱 구현
LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다.

주요 기능:
- 네이버 지도 연동 맛집 추가
- 랜덤 메뉴 추천 시스템
- 날씨 기반 거리 조정
- 방문 기록 관리
- Bluetooth 맛집 공유
- 다크모드 지원

기술 스택:
- Flutter 3.8.1+
- Riverpod 상태 관리
- Hive 로컬 DB
- Clean Architecture

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-30 19:03:28 +09:00

14 KiB

코드 컨벤션 문서

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 의미있는 이름 작성

// ❌ 나쁜 예
String n = "김밥천국";
int d = 500;
List<Restaurant> r = [];

// ✅ 좋은 예
String restaurantName = "김밥천국";
int maxDistanceInMeters = 500;
List<Restaurant> nearbyRestaurants = [];

3.3 Boolean 변수 네이밍

// ❌ 나쁜 예
bool loading = true;
bool error = false;

// ✅ 좋은 예
bool isLoading = true;
bool hasError = false;
bool canDelete = true;
bool shouldRefresh = false;

3.4 함수/메서드 네이밍

// 동사로 시작
Future<void> saveRestaurant(Restaurant restaurant);
Future<List<Restaurant>> fetchNearbyRestaurants();
bool validateUrl(String url);
void clearCache();

// 상태 확인 메서드는 is/has/can으로 시작
bool isValidNaverUrl(String url);
bool hasLocationPermission();
bool canProcessUrl(String url);

4. 파일 구조 및 조직

4.1 파일 구조 템플릿

// 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 클래스 내부 구조

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<Restaurant> processUrl(String url) async {
    // 구현
  }
  
  // 8. private 메서드
  Future<String> _resolveUrl(String url) async {
    // 구현
  }
  
  // 9. 정적 메서드
  static bool isValidUrl(String url) {
    // 구현
  }
}

5. 코딩 스타일

5.1 들여쓰기 및 정렬

// 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자)
  • 긴 문자열은 여러 줄로 분할
// ❌ 나쁜 예
String errorMessage = '네이버 지도 URL 파싱 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';

// ✅ 좋은 예
String errorMessage = '네이버 지도 URL 파싱 중 오류가 발생했습니다. '
    '잠시 후 다시 시도해주세요.';

5.3 중괄호 사용

// 한 줄이어도 중괄호 사용
if (isValid) {
  return true;
}

// 여러 조건은 읽기 쉽게 정렬
if (restaurant.name.isNotEmpty &&
    restaurant.category.isNotEmpty &&
    restaurant.latitude != 0) {
  // 처리
}

5.4 빈 줄 사용

class RestaurantService {
  final RestaurantRepository _repository;
  final CacheManager _cache;
  
  RestaurantService(this._repository, this._cache);
  
  // 메서드 사이에 빈 줄
  Future<List<Restaurant>> getNearbyRestaurants() async {
    // 구현
  }
  
  Future<void> addRestaurant(Restaurant restaurant) async {
    // 구현
  }
}

6. 주석 작성 규칙

6.1 문서 주석

/// 네이버 단축 URL을 처리하여 식당 정보를 추출합니다.
/// 
/// [url]은 네이버 지도 또는 naver.me 단축 URL이어야 합니다.
/// 
/// 처리 과정:
/// 1. URL 유효성 검증
/// 2. 단축 URL 리다이렉션
/// 3. HTML 스크래핑
/// 4. 네이버 로컬 API 검색
/// 5. 정보 병합
/// 
/// 실패 시 [NaverUrlException]을 던집니다.
Future<Restaurant> processNaverUrl(String url) async {
  // 구현
}

6.2 인라인 주석

// 단축 URL인 경우 리다이렉션 처리
if (url.contains('naver.me')) {
  url = await _resolveShortUrl(url);
}

// TODO: 캐싱 로직 추가 필요
// FIXME: 타임아웃 처리 개선 필요
// HACK: CORS 우회를 위한 임시 방법

6.3 주석 작성 원칙

  • Why, not What: 코드가 무엇을 하는지가 아닌 왜 그렇게 하는지 설명
  • 한국어 사용: 모든 주석은 한국어로 작성
  • 최신 상태 유지: 코드 변경 시 주석도 함께 업데이트

7. 에러 처리

7.1 예외 정의

// 계층별 예외 클래스 정의
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 에러 처리 패턴

Future<Restaurant?> 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 사용

// ✅ 좋은 예
Future<void> 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 동시 실행

// 병렬 실행이 가능한 경우
Future<void> initializeApp() async {
  final results = await Future.wait([
    _loadUserSettings(),
    _fetchRestaurants(),
    _checkLocationPermission(),
  ]);
}

// 순차 실행이 필요한 경우
Future<void> processInOrder() async {
  final user = await _loadUser();
  final settings = await _loadUserSettings(user.id);
  final restaurants = await _fetchUserRestaurants(user.id);
}

9. 상태 관리 (Riverpod)

9.1 Provider 네이밍

// Provider 이름은 제공하는 값 + Provider
final restaurantListProvider = FutureProvider<List<Restaurant>>((ref) {
  return ref.watch(restaurantRepositoryProvider).getAllRestaurants();
});

// StateNotifierProvider는 notifier 추가
final restaurantFilterNotifierProvider = 
    StateNotifierProvider<RestaurantFilterNotifier, RestaurantFilter>((ref) {
  return RestaurantFilterNotifier();
});

9.2 Provider 구조화

// providers/restaurant_provider.dart
final restaurantRepositoryProvider = Provider<RestaurantRepository>((ref) {
  return RestaurantRepositoryImpl();
});

final nearbyRestaurantsProvider = FutureProvider<List<Restaurant>>((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 테스트 파일 구조

// 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<NaverUrlException>()),
      );
    });
  });
}

10.2 테스트 네이밍

// 테스트 이름은 한국어로 명확하게
test('레스토랑 이름이 비어있으면 예외를 던져야 함', () {});
test('중복된 레스토랑은 추가되지 않아야 함', () {});
test('거리 계산이 정확해야 함', () {});

11. 성능 최적화 규칙

11.1 위젯 최적화

// 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 메모리 관리

class RestaurantService {
  // 캐시 크기 제한
  final _cache = LruMap<String, Restaurant>(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 박스 이름은 상수로 정의
  • 마이그레이션 전략 문서화
  • 트랜잭션 단위로 처리