Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/sessions/2026-04-12_1_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# CI/CD 안정화 - tiggle, calynda, AIVA-SaaS
> 날짜: 2026-04-12 | 회차: 1 | 상태: 진행중

## 요청 내용
tiggle, calynda, aiav-bb(AIVA-SaaS) 세 프로젝트의 GitHub Actions CI가 자주 실패하는 원인 분석 및 안정화

## 영향 범위 분석

### Tiggle (72% CI 실패율)
- **근본 원인**: `TypeTag.tsx` 파일 누락 → 모든 FE 빌드 실패
- **부가 원인**: `DetailPage/index.tsx` prettier 포맷팅 오류, `NotFoundPage.tsx` import 순서 오류
- 변경 파일:
- `frontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsx` (복구)
- `frontend/tiggle/src/pages/DetailPage/index.tsx` (prettier 수정)
- `frontend/tiggle/src/pages/NotFoundPage.tsx` (import order 수정)

### Calynda (CI 통과 중, 개선 필요)
- **이슈**: FE(Flutter) 테스트가 CI에 없음. 15개 테스트 파일 존재하지만 미실행
- 변경 파일:
- `be/.github/workflows/ci.yml` (frontend-test job 추가)
- `be/.github/workflows/deploy-nas.yml` (test-frontend gate job 추가)

### AIVA-SaaS (현재 안정 - 최근 5회 연속 성공)
- 과거 이슈: Flyway 마이그레이션 충돌, BLoC 상태관리 버그 (해결됨)
- 현재 안정 상태 → 즉시 수정 불필요

## 작업 계획
| # | 작업 | 프로젝트 | 파일 | 난이도 |
|---|------|---------|------|--------|
| 1 | TypeTag.tsx 복구 | Tiggle | atoms/TypeTag/TypeTag.tsx | S |
| 2 | prettier/import 오류 수정 | Tiggle | DetailPage, NotFoundPage | S |
| 3 | FE 빌드 + BE 테스트 로컬 검증 | Tiggle | - | S |
| 4 | Flutter 테스트 CI 추가 | Calynda | ci.yml, deploy-nas.yml | M |
| 5 | AIVA-SaaS 현황 분석 | AIVA-SaaS | - | S |

## 성능 설계
- CI 실행 시간 영향: Flutter 테스트 추가로 Calynda CI ~2-3분 증가 (허용 범위)
- Calynda deploy에서 test-frontend이 test-backend과 병렬 실행되므로 총 시간 증가 최소화

## 검증 계획
- [x] Tiggle BE 테스트 통과 (`./gradlew :tiggle:test`)
- [x] Tiggle FE 빌드 통과 (`npm run build`)
- [x] Calynda CI YAML 문법 검증
- [ ] Tiggle 커밋 후 CI 통과 확인 (GitHub Actions)
- [ ] Calynda 커밋 후 CI 통과 확인 (GitHub Actions)
66 changes: 66 additions & 0 deletions docs/sessions/2026-04-12_1_result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CI/CD 안정화 - tiggle, calynda, AIVA-SaaS - 결과
> 날짜: 2026-04-12 | 회차: 1 | 상태: 완료 (Calynda PR 수동 머지 대기)

## 변경 사항

### Tiggle (kdh-92/Tiggle)
| 파일 | 변경 유형 | 설명 |
|------|----------|------|
| `frontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsx` | 복구 | 86761c1에서 실수로 삭제된 컴포넌트 복원 |
| `frontend/tiggle/src/pages/DetailPage/index.tsx` | 수정 | prettier 인라인 style 포맷팅 오류 수정 |
| `frontend/tiggle/src/pages/NotFoundPage.tsx` | 수정 | import 그룹 간 빈 줄 추가 (import/order 규칙) |
| `docs/sessions/2026-04-12_1_plan.md` | 신규 | 기획서 |

### Calynda BE (calynda-app/be)
| 파일 | 변경 유형 | 설명 |
|------|----------|------|
| `.github/workflows/ci.yml` | 수정 | frontend-test job 추가 (flutter test) |
| `.github/workflows/deploy-nas.yml` | 수정 | test-frontend gate job 추가, deploy-infra/deploy-frontend의 needs에 추가 |

### Calynda FE (calynda-app/fe)
| 파일 | 변경 유형 | 설명 |
|------|----------|------|
| `pubspec.yaml` | 수정 | mockito ^5.4.6 dev dependency 추가 |
| `pubspec.lock` | 수정 | 의존성 잠금 갱신 |
| `test/helpers/mocks.mocks.dart` | 수정 | mockito mock 재생성 |

### AIVA-SaaS
변경 없음 (현재 안정 상태)

## 커밋 이력
- **Tiggle**: `db404f6` fix(fe): restore TypeTag.tsx and fix lint errors blocking CI
- **Calynda BE**: `572e358` ci: add Flutter test gate to CI and deploy workflows
- **Calynda FE**: `2e18dcb` fix(test): add mockito dev dependency for test execution
- **Calynda FE**: `509cb04` chore: regenerate mock files after mockito dependency added

## PR 현황
| 프로젝트 | PR | 상태 | CI |
|---------|-----|------|-----|
| Tiggle | [#228](https://github.com/kdh-92/Tiggle/pull/228) | 머지 대기 (admin 권한 필요) | PASS (BE 2m57s, FE 2m21s) |
| Calynda FE | 수동 생성 필요 | 브랜치 push 완료 (`fix/add-mockito-dep`) | 로컬 PASS (127/32 skip) |
| Calynda BE | 수동 생성 필요 | 브랜치 push 완료 (`fix/ci-add-flutter-tests`) | FE PR 먼저 머지 필요 |

## 검증 결과
| 항목 | 결과 | 비고 |
|------|------|------|
| Tiggle BE 테스트 | PASS | `./gradlew :tiggle:test` BUILD SUCCESSFUL |
| Tiggle FE lint | PASS | `npm run lint` |
| Tiggle FE 빌드 | PASS | `npm run build` 7.25s |
| Tiggle CI (GitHub) | PASS | backend-test + frontend-test + CodeRabbit |
| Calynda Flutter 테스트 | PASS | 127 passed, 32 skipped (API 통합 테스트) |
| AIVA-SaaS CI | PASS | 최근 5회 연속 성공, 변경 불필요 |

## 발견된 추가 이슈
1. **Calynda FE `flutter analyze` 에러 36건**: `dividingSpace` 함수 시그니처 변경으로 인한 기존 코드 에러. CI에서 analyze 제외함. 별도 수정 필요.
2. **gh CLI 계정 불일치**: SSH는 `kdh-92` 계정, gh CLI는 `kdh929624` 계정. private 레포(calynda-app) PR 생성/머지 불가. gh CLI 토큰 설정 확인 필요.
3. **Tiggle FE 번들 크기 경고**: 1,069KB (gzip 331KB). code-splitting 미적용. 기능에 영향 없으나 개선 권장.

## 머지 순서 (수동)
1. Tiggle PR #228 머지 (GitHub에서 admin 머지)
2. Calynda FE `fix/add-mockito-dep` PR 생성 및 머지
3. Calynda BE `fix/ci-add-flutter-tests` PR 생성 및 머지 (2번 이후)

## 롤백 정보
- Tiggle: `git revert db404f6`
- Calynda BE: `git revert 572e358`
- Calynda FE: `git revert 509cb04 2e18dcb`
33 changes: 33 additions & 0 deletions frontend/tiggle/src/components/atoms/TypeTag/TypeTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HTMLAttributes } from "react";

import cn from "classnames";

import { TypeTagStyle } from "@/components/atoms/TypeTag/TypeTagStyle";
import { Tx, TxType } from "@/types";

interface TypeTagProps extends HTMLAttributes<HTMLDivElement> {
size: "md" | "lg";
txType: TxType;
}

export default function TypeTag({
size,
txType,
className,
...props
}: TypeTagProps) {
return (
<TypeTagStyle
className={cn("type-tag", txType, className, size)}
{...props}
>
<p className="label">
{txType === Tx.OUTCOME
? "지출"
: txType === Tx.REFUND
? "환불"
: "수익"}
</p>
</TypeTagStyle>
);
}
109 changes: 57 additions & 52 deletions frontend/tiggle/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,62 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export { ApiError } from "./core/ApiError";
export { CancelablePromise, CancelError } from "./core/CancelablePromise";
export { OpenAPI } from "./core/OpenAPI";
export type { OpenAPIConfig } from "./core/OpenAPI";

export type { ApiResponse } from './models/ApiResponse';
export type { ApiResponseCategoryListRespDto } from './models/ApiResponseCategoryListRespDto';
export type { ApiResponseCommentPageRespDto } from './models/ApiResponseCommentPageRespDto';
export type { ApiResponseListNotificationRespDto } from './models/ApiResponseListNotificationRespDto';
export type { ApiResponseListTagRespDto } from './models/ApiResponseListTagRespDto';
export type { ApiResponseMapStringObject } from './models/ApiResponseMapStringObject';
export type { ApiResponseMemberListRespDto } from './models/ApiResponseMemberListRespDto';
export type { ApiResponseMemberRespDto } from './models/ApiResponseMemberRespDto';
export type { ApiResponseReactionSummaryRespDto } from './models/ApiResponseReactionSummaryRespDto';
export type { ApiResponseTagRespDto } from './models/ApiResponseTagRespDto';
export type { ApiResponseTokenResponse } from './models/ApiResponseTokenResponse';
export type { ApiResponseTransactionPageRespDto } from './models/ApiResponseTransactionPageRespDto';
export type { ApiResponseTransactionRespDto } from './models/ApiResponseTransactionRespDto';
export type { ApiResponseUnit } from './models/ApiResponseUnit';
export type { CategoryCreateReqDto } from './models/CategoryCreateReqDto';
export type { CategoryListRespDto } from './models/CategoryListRespDto';
export type { CategoryRespDto } from './models/CategoryRespDto';
export type { CategoryUpdateReqDto } from './models/CategoryUpdateReqDto';
export type { CommentChildRespDto } from './models/CommentChildRespDto';
export type { CommentCreateReqDto } from './models/CommentCreateReqDto';
export type { CommentPageRespDto } from './models/CommentPageRespDto';
export type { CommentRespDto } from './models/CommentRespDto';
export type { CommentUpdateReqDto } from './models/CommentUpdateReqDto';
export type { MemberCreateReqDto } from './models/MemberCreateReqDto';
export type { MemberInfo } from './models/MemberInfo';
export type { MemberListRespDto } from './models/MemberListRespDto';
export type { MemberRespDto } from './models/MemberRespDto';
export type { MemberUpdateReqDto } from './models/MemberUpdateReqDto';
export type { NotificationRespDto } from './models/NotificationRespDto';
export type { ReactionCreateReqDto } from './models/ReactionCreateReqDto';
export type { ReactionSummaryRespDto } from './models/ReactionSummaryRespDto';
export type { TagCreateReqDto } from './models/TagCreateReqDto';
export type { TagRespDto } from './models/TagRespDto';
export type { TagUpdateReqDto } from './models/TagUpdateReqDto';
export type { TokenResponse } from './models/TokenResponse';
export type { TransactionCreateReqDto } from './models/TransactionCreateReqDto';
export type { TransactionDtoWithCount } from './models/TransactionDtoWithCount';
export type { TransactionPageRespDto } from './models/TransactionPageRespDto';
export type { TransactionRespDto } from './models/TransactionRespDto';
export type { TransactionUpdateReqDto } from './models/TransactionUpdateReqDto';
export type { ApiResponse } from "./models/ApiResponse";
export type { ApiResponseCategoryListRespDto } from "./models/ApiResponseCategoryListRespDto";
export type { ApiResponseCommentPageRespDto } from "./models/ApiResponseCommentPageRespDto";
export type { ApiResponseListNotificationRespDto } from "./models/ApiResponseListNotificationRespDto";
export type { ApiResponseListTagRespDto } from "./models/ApiResponseListTagRespDto";
export type { ApiResponseMapStringObject } from "./models/ApiResponseMapStringObject";
export type { ApiResponseMemberListRespDto } from "./models/ApiResponseMemberListRespDto";
export type { ApiResponseMemberRespDto } from "./models/ApiResponseMemberRespDto";
export type { ApiResponseReactionSummaryRespDto } from "./models/ApiResponseReactionSummaryRespDto";
export type { ApiResponseTagRespDto } from "./models/ApiResponseTagRespDto";
export type { ApiResponseTokenResponse } from "./models/ApiResponseTokenResponse";
export type { ApiResponseTransactionPageRespDto } from "./models/ApiResponseTransactionPageRespDto";
export type { ApiResponseTransactionRespDto } from "./models/ApiResponseTransactionRespDto";
export type { ApiResponseUnit } from "./models/ApiResponseUnit";
export type { CategoryCreateReqDto } from "./models/CategoryCreateReqDto";
export type { CategoryListRespDto } from "./models/CategoryListRespDto";
export type { CategoryRespDto } from "./models/CategoryRespDto";
export type { CategoryUpdateReqDto } from "./models/CategoryUpdateReqDto";
export type { CommentChildRespDto } from "./models/CommentChildRespDto";
export type { CommentCreateReqDto } from "./models/CommentCreateReqDto";
export type { CommentPageRespDto } from "./models/CommentPageRespDto";
export type { CommentRespDto } from "./models/CommentRespDto";
export type { CommentUpdateReqDto } from "./models/CommentUpdateReqDto";
export type { MemberCreateReqDto } from "./models/MemberCreateReqDto";
export type { MemberInfo } from "./models/MemberInfo";
export type { MemberListRespDto } from "./models/MemberListRespDto";
export type { MemberRespDto } from "./models/MemberRespDto";
export type { MemberUpdateReqDto } from "./models/MemberUpdateReqDto";
export type { NotificationRespDto } from "./models/NotificationRespDto";
export type { ReactionCreateReqDto } from "./models/ReactionCreateReqDto";
export type { ReactionSummaryRespDto } from "./models/ReactionSummaryRespDto";
export type { TagCreateReqDto } from "./models/TagCreateReqDto";
export type { TagRespDto } from "./models/TagRespDto";
export type { TagUpdateReqDto } from "./models/TagUpdateReqDto";
export type { TokenResponse } from "./models/TokenResponse";
export type { TransactionCreateReqDto } from "./models/TransactionCreateReqDto";
export type { TransactionDtoWithCount } from "./models/TransactionDtoWithCount";
export type { TransactionPageRespDto } from "./models/TransactionPageRespDto";
export type { TransactionRespDto } from "./models/TransactionRespDto";
export type { TransactionUpdateReqDto } from "./models/TransactionUpdateReqDto";

export { AuthControllerService } from './services/AuthControllerService';
export { CategoryApiControllerService } from './services/CategoryApiControllerService';
export { CommentApiService } from './services/CommentApiService';
export { MemberApiControllerService } from './services/MemberApiControllerService';
export { NotificationApiControllerService } from './services/NotificationApiControllerService';
export { ReactionApiService } from './services/ReactionApiService';
export { TagApiControllerService } from './services/TagApiControllerService';
export { TransactionApiControllerService } from './services/TransactionApiControllerService';
export { AuthControllerService } from "./services/AuthControllerService";
export { CategoryApiControllerService } from "./services/CategoryApiControllerService";
export { CommentApiService } from "./services/CommentApiService";
export { MemberApiControllerService } from "./services/MemberApiControllerService";
export { NotificationApiControllerService } from "./services/NotificationApiControllerService";
export { ReactionApiService } from "./services/ReactionApiService";
export { TagApiControllerService } from "./services/TagApiControllerService";
export { TransactionApiControllerService } from "./services/TransactionApiControllerService";
export { StatisticsApiControllerService } from "./services/StatisticsApiControllerService";
export { CharacterApiControllerService } from "./services/CharacterApiControllerService";
export { ItemApiControllerService } from "./services/ItemApiControllerService";
export { AchievementApiControllerService } from "./services/AchievementApiControllerService";
export { ChallengeApiControllerService } from "./services/ChallengeApiControllerService";
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CancelablePromise } from "../core/CancelablePromise";
import { OpenAPI } from "../core/OpenAPI";
import { request as __request } from "../core/request";

import type { ApiResponse } from "../models/ApiResponse";

export class AchievementApiControllerService {
/**
* 전체 업적 목록 (달성 여부 포함)
* @returns ApiResponse 업적 목록
* @throws ApiError
*/
public static getAchievements(): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/achievements",
});
}

/**
* 최근 달성 업적
* @param limit 조회 개수
* @returns ApiResponse 최근 업적 목록
* @throws ApiError
*/
public static getRecentAchievements(
limit: number = 5,
): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/achievements/recent",
query: {
limit: limit,
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CancelablePromise } from "../core/CancelablePromise";
import { OpenAPI } from "../core/OpenAPI";
import { request as __request } from "../core/request";

import type { ApiResponse } from "../models/ApiResponse";

export class ChallengeApiControllerService {
/**
* 챌린지 생성
* @param requestBody 챌린지 생성 요청
* @returns ApiResponse 생성된 챌린지
* @throws ApiError
*/
public static createChallenge(requestBody: {
type: string;
targetDays: number;
}): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "POST",
url: "/api/v1/challenges",
body: requestBody,
mediaType: "application/json",
});
}

/**
* 진행 중 챌린지 조회
* @returns ApiResponse 현재 활성 챌린지
* @throws ApiError
*/
public static getActiveChallenge(): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/challenges/active",
});
}

/**
* 챌린지 상세 (일별 로그 포함)
* @param id 챌린지 ID
* @returns ApiResponse 챌린지 상세 정보
* @throws ApiError
*/
public static getChallengeDetail(id: number): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/challenges/{id}",
path: {
id: id,
},
});
}

/**
* 완료/실패 챌린지 목록
* @param page 페이지 번호
* @param size 페이지 크기
* @returns ApiResponse 챌린지 히스토리
* @throws ApiError
*/
public static getChallengeHistory(
page: number = 0,
size: number = 10,
): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/api/v1/challenges/history",
query: {
page: page,
size: size,
},
});
}

/**
* 챌린지 취소
* @param id 챌린지 ID
* @returns ApiResponse OK
* @throws ApiError
*/
public static cancelChallenge(id: number): CancelablePromise<ApiResponse> {
return __request(OpenAPI, {
method: "DELETE",
url: "/api/v1/challenges/{id}",
path: {
id: id,
},
});
}
}
Loading
Loading