기여 가이드

기여 가이드 문서

CheonYakPlanet 백엔드 관련 문서입니다.

Contributing Guidelines


🤝 Welcome Contributors


Thank you for your interest in contributing to CheonYakPlanet! This guide will help you understand our development process, coding standards, and how to submit quality contributions.


📋 Table of Contents


  1. Getting Started
  2. Development Workflow
  3. Coding Standards
  4. Testing Requirements
  5. Code Review Process
  6. Commit Message Guidelines
  7. Pull Request Guidelines
  8. Korean Business Domain Guidelines

🚀 Getting Started


Prerequisites


Initial Setup

1. Fork the Repository

# Fork on GitHub, then clone your fork
git clone https://github.com/your-username/cheonyakplanet-be.git
cd cheonyakplanet-be

2. Set Up Development Environment

# Add upstream remote
git remote add upstream https://github.com/original-org/cheonyakplanet-be.git

# Install dependencies and run tests
./gradlew build
./gradlew test

3. Configure Environment


4. Read Documentation


🔄 Development Workflow


Branch Strategy

We follow Git Flow with Korean feature naming:


# Feature branches (Korean names encouraged)
feature/청약-알림-시스템
feature/subscription-alert-system

# Bugfix branches
bugfix/fix-duplicate-subscription-error

# Hotfix branches
hotfix/security-jwt-vulnerability

# Release branches
release/v1.1.0

Development Process

1. Create Feature Branch

git checkout develop
git pull upstream develop
git checkout -b feature/your-feature-name

2. Develop with TDD


3. Follow Clean Architecture


4. Commit Frequently


5. Push and Create PR

git push origin feature/your-feature-name
# Create Pull Request on GitHub

📝 Coding Standards


Java Code Style


Class Structure
@Entity
@Table(catalog = "planet", name = "example_entity")
@EntityListeners({AuditingEntityListener.class, SoftDeleteListener.class})
public class ExampleEntity extends Stamped {

    // Static fields first
    private static final String CONSTANT_VALUE = "value";

    // Instance fields
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // Constructors
    public ExampleEntity() {}

    public ExampleEntity(String name) {
        this.name = name;
    }

    // Public methods
    public void performBusinessOperation() {
        validateBusinessRules();
        // Business logic here
    }

    // Private methods
    private void validateBusinessRules() {
        if (name == null || name.trim().isEmpty()) {
            throw new CustomException(ErrorCode.VALIDATION_ERROR, "Name is required");
        }
    }

    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

Service Layer Standards
@Service
@Transactional(readOnly = true) // Default to read-only
@Slf4j
public class ExampleService {

    private final ExampleRepository repository;

    public ExampleService(ExampleRepository repository) {
        this.repository = repository;
    }

    @Transactional // Override for write operations
    public ExampleDTO createExample(CreateExampleDTO dto, UserDetailsImpl userDetails) {
        // 1. Validation
        validateAuthentication(userDetails);
        validateInput(dto);

        // 2. Business logic
        ExampleEntity entity = ExampleEntity.builder()
                .name(dto.getName())
                .build();

        // 3. Persistence
        ExampleEntity saved = repository.save(entity);

        // 4. Return DTO
        return ExampleDTO.fromEntity(saved);
    }

    private void validateAuthentication(UserDetailsImpl userDetails) {
        if (userDetails == null) {
            throw new CustomException(ErrorCode.AUTH001, "인증이 필요한 서비스입니다");
        }
    }

    private void validateInput(CreateExampleDTO dto) {
        if (dto.getName() == null || dto.getName().trim().isEmpty()) {
            throw new CustomException(ErrorCode.VALIDATION_ERROR, "이름은 필수입니다");
        }
    }
}

Controller Standards
@RestController
@RequestMapping("/api/examples")
@Tag(name = "Example API", description = "예제 관리 API")
public class ExampleController extends BaseController {

    private final ExampleService service;

    public ExampleController(ExampleService service) {
        this.service = service;
    }

    @PostMapping
    @Operation(summary = "예제 생성", description = "새로운 예제를 생성합니다")
    public ResponseEntity createExample(
            @RequestBody @Valid CreateExampleDTO dto,
            @AuthenticationPrincipal UserDetailsImpl userDetails) {

        ExampleDTO result = service.createExample(dto, userDetails);
        return ResponseEntity.ok(success(result));
    }
}

Naming Conventions


Korean Business Terms

Use Korean terms for business concepts:

// Good - Korean business terms
private String houseNm; // 주택명
private LocalDate rceptBgnde; // 접수시작일
private String spsplyType; // 특별공급유형

// Avoid - Direct English translation
private String houseName;
private LocalDate receptionStartDate;
private String specialSupplyType;

General Naming

Error Handling Standards


Custom Exceptions
// Always use specific error codes
throw new CustomException(ErrorCode.SUB001, "청약 정보를 찾을 수 없습니다");

// Include additional context when helpful
throw new CustomException(ErrorCode.AUTH002, "권한이 없습니다",
        "Required role: ADMIN, Current role: " + userRole);

Error Codes

Follow the established pattern:


🧪 Testing Requirements


Test Coverage Standards


Test Structure (Given-When-Then)

@ExtendWith(MockitoExtension.class)
@DisplayName("구독 서비스 테스트")
class SubscriptionServiceTest {

    @Mock
    private SubscriptionRepository repository;

    @InjectMocks
    private SubscriptionService service;

    @Test
    @DisplayName("구독 생성 - 성공: 유효한 입력값")
    void givenValidInput_whenCreateSubscription_thenReturnSubscriptionDTO() {
        // Given
        CreateSubscriptionDTO input = CreateSubscriptionDTO.builder()
                .houseName("래미안 강남포레스트")
                .region("서울특별시")
                .city("강남구")
                .build();

        SubscriptionInfo entity = createTestSubscription();
        given(repository.save(any(SubscriptionInfo.class))).willReturn(entity);

        // When
        SubscriptionDTO result = service.createSubscription(input);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getHouseName()).isEqualTo("래미안 강남포레스트");
        then(repository).should().save(any(SubscriptionInfo.class));
    }

    @Test
    @DisplayName("구독 생성 - 실패: 중복된 주택관리번호")
    void givenDuplicateHouseManageNo_whenCreateSubscription_thenThrowException() {
        // Given
        CreateSubscriptionDTO input = createDuplicateInput();
        given(repository.existsByHouseManageNo(anyString())).willReturn(true);

        // When & Then
        CustomException exception = assertThrows(CustomException.class, () -> {
            service.createSubscription(input);
        });

        assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.SUB003);
        then(repository).should(never()).save(any());
    }

    private SubscriptionInfo createTestSubscription() {
        return SubscriptionInfo.builder()
                .houseNm("래미안 강남포레스트")
                .houseManageNo("2024000001")
                .region("서울특별시")
                .city("강남구")
                .build();
    }
}

Korean Domain Test Data

Use realistic Korean data in tests:

private User createTestUser() {
    return User.builder()
            .email("test@cheonyakplanet.com")
            .username("청약초보")
            .role(UserRoleEnum.USER)
            .monthlyIncome(500) // 500만원
            .isMarried(true) // 기혼
            .numChild(2) // 자녀 2명
            .hasHouse(false) // 무주택
            .interestLocal1("서울특별시 강남구")
            .interestLocal2("경기도 성남시")
            .build();
}

👀 Code Review Process


Before Submitting PR


Review Checklist


Architecture & Design

Code Quality

Korean Domain Accuracy

Testing

Security

Review Response Guidelines


For Reviewers

For Contributors

📝 Commit Message Guidelines


Types


Examples

feat(subscription): add real-time subscription alerts

- Implement WebSocket-based notification system
- Add subscription preference settings
- Include email fallback for offline users

Closes #123

fix(auth): resolve JWT token refresh issue

- Fix token expiry calculation bug
- Add proper error handling for expired tokens
- Update token refresh endpoint tests

test(community): add comprehensive post creation tests

- Test Korean character handling in posts
- Add validation error scenarios
- Include pagination edge cases

refactor(news): improve content filtering pipeline

- Extract filtering logic into separate service
- Add configurable filtering rules
- Improve performance with parallel processing

🔍 Pull Request Guidelines


PR Title Format

[Type] Brief description of changes

Examples:
[Feature] Add subscription price comparison tool
[Fix] Resolve Korean address parsing issue
[Refactor] Improve news content filtering performance

PR Description Template

## 📋 Summary
Brief description of what this PR does.

## 🎯 Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update

## 🔧 Changes Made
- Change 1
- Change 2
- Change 3

## 🧪 Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing performed
- [ ] Korean business logic validated

## 📚 Documentation
- [ ] Code comments updated
- [ ] API documentation updated
- [ ] User documentation updated (if applicable)

## 🚀 Deployment Notes
Any special deployment considerations.

## 📱 Screenshots (if applicable)
Screenshots of UI changes.

## ✅ Checklist
- [ ] Code follows project coding standards
- [ ] Self-review completed
- [ ] Tests pass locally
- [ ] Korean business requirements met
- [ ] Breaking changes documented

PR Size Guidelines


🏢 Korean Business Domain Guidelines


Understanding Korean Real Estate


Korean Text Handling

// Good - Proper Korean text validation
private void validateKoreanText(String text) {
    if (text == null || text.trim().isEmpty()) {
        throw new CustomException(ErrorCode.VALIDATION001, "입력값이 필요합니다");
    }

    // Check for Korean characters if needed
    if (!text.matches(".*[ㄱ-ㅎㅏ-ㅣ가-힣].*")) {
        throw new CustomException(ErrorCode.VALIDATION002, "한글 입력이 필요합니다");
    }
}

// Database columns for Korean text
@Column(columnDefinition = "TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
private String koreanContent;

Korean Date and Number Formats

// Date formatting for Korean users
DateTimeFormatter koreanDateFormat = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");

// Number formatting for Korean currency
DecimalFormat koreanCurrency = new DecimalFormat("#,###원");

🆘 Getting Help


Communication Channels


When to Ask for Help


📈 Contribution Recognition


Types of Contributions


Recognition




📚 Additional Resources





Contributing Guidelines Version: 1.0

Last Updated: 2025-06-26

Next Review: 2025-09-19


Thank you for contributing to CheonYakPlanet! 🏠🇰🇷