Django / GORM exporter 완결성 보완 - #169
Conversation
Conflict resolution: kept GORM exporter support added in fork while integrating upstream's 0.2.0 API changes, LSP features, newtype identifiers, and refactored test structure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds tests for all branches identified as uncovered (59 lines): - Django: SmallAutoField, BigAutoField, Macaddr, Numeric, Custom type, UUID functional default, export() multi-table, nullable FK with db_column - GORM: conflicting enum qualified names, Char type tag, FK relation field name collision, reverse relation disambiguation (two FKs same target) - CLI: OrmArg::Django mapping, build_output_path Gorm .go extension, clean_export_dir Gorm .go cleanup Also removes the erroneous targets line from rust-toolchain.toml. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `cache-key: no-musl` to every `setup-rust-toolchain@v1` step that does not already specify an explicit cross-compilation target. This changes the Rust toolchain cache key so that the previous cache (written when rust-toolchain.toml briefly had `targets = ["x86_64-unknown-linux-musl"]`) is not restored, eliminating the recurring "override toolchain 'stable-x86_64-unknown-linux-musl' is not installed" error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sea-orm v2.0.0-rc.42 drops its dependency on ouroboros v0.18.5 (and aliasable, id-arena). ouroboros has a RUSTSEC advisory for unsound self-referential structs; without an explicit ignore entry in deny.toml the cargo-deny CI gate was flagging it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h 100%
Django: build_default Bool(false) and functional-default-on-non-special-type
branches; reference_action_str Restrict/SetDefault/NoAction arms; column
comment rendering; unnamed Index and unnamed composite UniqueConstraint in
Meta; to_pascal_case None arm via double-underscore input.
GORM: Numeric column (add_column_type, build_gorm_tag, decimal import);
unnamed and named Index (collect_index_info body + build_gorm_tag loop);
auto-named composite unique (collect_composite_unique_info None closure);
singular source-table plural (find_reverse_relations format!("{pascal}s"));
FK on_update body + nullable FK pointer type (render_fk_relation_field);
reference_action_str SetNull/SetDefault/NoAction; to_pascal_case None arm
via double-underscore table name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enerator arb_default_string() could produce reserved words like "in" as bare SQL DEFAULT expressions, causing pg_query to reject the emitted CREATE TABLE with "syntax error at or near 'in'". Added is_pg_reserved_keyword() (full PG 17 §C.1 Type-A list) and a prop_filter on the bare-ident branch so the strategy only generates non-reserved identifiers as unquoted defaults. Also fixes fmt issues in the coverage tests added in the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- erd: add test for inline FK referencing absent parent table, covering the None early-return branch of inline_foreign_key_relation - gorm/django: convert single-expression #[cfg(not(tarpaulin_include))] arms to block form so tarpaulin's source-level exclusion correctly identifies and skips the non_exhaustive future-variant guards Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bution erd: normalize_tables' map(closure).collect() becomes an explicit for loop (the map closure was the same LLVM source-coverage attribution blind spot documented in this crate's AGENTS.md; the prior fix only made the inner with_context closure eager but left the outer map closure in place). django: replace the enum max_length iterator chain (map(String::len).max()) with an explicit loop, and fold the single-statement primary_key/unique kwargs into a for-loop over conditions instead of standalone trivial ifs. gorm: render_enum's match was the last statement of a unit-returning function; convert to sequential if let with an early return so the function body ends on a plain statement instead of a match tail-expression. All three spots are proven to execute today (existing snapshots already show unique=True, max_length=9, and rendered enum consts), so this is a pure coverage-attribution fix with no behavior change — snapshots are byte-identical and all local build/test/clippy/fmt checks pass. Local tarpaulin isn't runnable on Windows, so the 100% gate result is confirmed by CI on push. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diagnosed via a local tarpaulin run (Docker, same xd009642/tarpaulin
container CI uses) inspecting raw LLVM coverage regions instead of
guessing from reformatted line numbers. Each of the 5 previously-uncovered
lines had a distinct, verified cause:
- erd: normalize_tables' `?` error-propagation branch was never exercised
by any test (all existing tests only feed valid tables). Added a test
with a malformed inline FK reference to trigger normalize()'s Err path.
- erd: collect_foreign_key_relations' table-level FK `let-else { continue }`
branch (absent referenced table) was untested — only the *inline* FK
equivalent had a test (from 584b04b). Added the table-level counterpart.
- django: build_default's Bool(true) path was never tested (only
Bool(false) was). Added test_bool_true_default.
- django: build_default's `return match { guarded-arm => {...} }` construct
had a proven LLVM gap-region artifact on the match/guard header lines
(arm bodies demonstrably execute via existing tests, e.g.
test_server_default_timezone). Restructured into plain if-chains.
- gorm: render_enum's trailing if-let block's closing brace showed 0 hits
despite its body executing (same gap-region artifact); restructured to
collect into a Vec and lines.extend() it as a genuine trailing statement.
- gorm: go_base_type's ComplexColumnType::Enum arm was genuinely dead code
(its only caller, go_type_for_column_mapped, already intercepts Enum
before ever calling go_base_type) — removed.
Verified locally end-to-end: cargo tarpaulin --engine llvm against the
exact CI container reports erd/mod.rs 212/212, django/types.rs 94/94,
gorm/mod.rs 289/289 (100% each), with no other regressions across either
crate. cargo build/test/clippy/fmt and the line-budget check all pass on
the real (normally-formatted) source.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI fmt job flagged these two spots as unformatted (from 510e868); rustfmt --check now passes locally with no other diffs.
…hema DjangoExporter previously fell back to the OrmExporter trait's default render_entity_with_schema (schema-context ignored), so composite-PK junction tables never produced a ManyToManyField on either side. Detect 2-FK junction tables (mirroring the SeaORM junction-detection pattern) and emit ManyToManyField(..., through=..., related_name="+") for both sides, with _via_<junction> disambiguation when multiple junctions link the same pair of tables. Purely self-referential junctions are skipped rather than guessed at.
Both exporters only recognized single-column FKs (columns.len() == 1), so composite FKs silently dropped to plain scalar columns with no relation info at all. - GORM: composite FKs are a genuine native feature (comma-separated foreignKey/references tags), so emit a real belongs-to relation field, with numeric-suffix disambiguation on field-name collisions. - Django: there is no native multi-column FK field, so emit a comment documenting the relationship instead of silently dropping it; the underlying columns still render normally and referential integrity is enforced by the generated database schema. Reuses the existing crate::utils::python::collect_composite_fks helper already shared with SQLAlchemy.
find_reverse_relations() skipped any "other" table equal to the current
table name, which meant a self-referencing FK (e.g. categories.parent_id
-> categories.id) only ever produced the forward belongs-to relation
("Parent"), never the reverse has-many ("Children"). The forward FK and
reverse-scan loop are independent, so the skip was unconditionally
dropping half of every self-referential relationship.
Removed the skip and special-cased self-ref naming to "Children" instead
of a pluralized table name (which would otherwise collide with the
struct's own name). Added regression tests for both directions, split
into gorm/tests/relations.rs (composite-FK + self-ref tests) to keep
gorm/tests/mod.rs under the 1200-line test-file budget.
… suite Extends orm_cases! (60 fixtures) and render_entity_with_schema_snapshots (14 relation-heavy scenarios) to render through Gorm/Django alongside the existing 4 ORMs, matching the project's "every scenario cross-compared across all ORMs" convention. Adds 120 new baseline snapshots; all render successfully with no panics. Reviewing the new baselines surfaced two real, pre-existing Django correctness bugs (never caught because Django had zero cross-ORM fixture coverage before this): 1. build_default()'s final fallback emitted unrecognized SQL constants verbatim as a bare Python identifier (e.g. `default=SOME_CONSTANT`), which is an undefined name and would crash at Django import time. Now omits the default unless it parses as a numeric literal. 2. Any auto-increment primary key (AutoField/SmallAutoField/BigAutoField) was rendered WITHOUT `primary_key=True` on the assumption that the Auto*Field type alone implies it — it does not. Django's own system checks (fields.E100) reject an explicit AutoField without primary_key=True, so every auto-PK schema this exporter has ever produced was invalid at `manage.py check`. Fixed by always emitting primary_key=True when the column is the (non-composite) PK; removed the now-dead is_auto_field() helper and the auto_increment parameter it existed solely to feed.
Django and GORM had no vespertide.json config surface at all, unlike SeaOrmConfig. Adds two minimal, well-scoped knobs mirroring SeaOrmExporterWithConfig's existing pattern: - DjangoConfig.app_label: optional explicit `app_label` written into every generated model's Meta class, for projects where models don't live inside a standard Django app package (Django can't infer the label there). Omitted from Meta and from JSON when None. - GormConfig.package_name: Go package name emitted at the top of every file (`package <name>`), default "models". Both structs are #[non_exhaustive] and threaded through new DjangoExporterWithConfig / GormExporterWithConfig wrappers, wired into the CLI's cmd_export alongside the existing SeaOrmExporterWithConfig special-case. Regenerated schemas/config.schema.json (schema-drift CI gate) to include the two new sections.
SeaOrm/SqlAlchemy/SqlModel/Jpa/Gorm each had a dedicated clean_export_dir_removes_*_for_* test; Django was the only export target without one, even though it shares the .py cleanup path with SqlAlchemy/SqlModel.
Both had zero mentions of gorm/django (GORM had already merged before AGENTS.md's own generation date), and README's Features/CLI-usage lines listed GORM but not Django or JPA. - README.md: Features bullet and --orm CLI examples now list all six supported ORMs. - AGENTS.md: crate-tree comment and the ORM-export WHERE-TO-LOOK path now include gorm/django; corrected the now-stale "4 ORMs / 232 snapshots" figures in the orm_cases! section to the current 6 ORMs / 362 snapshots after wiring Django and GORM into the shared macro. - crates/vespertide-exporter/AGENTS.md: added GORM and Django to the crate summary/STRUCTURE tree, and two new BACKEND NOTES subsections documenting their relation-inference behavior, config knobs (GormExporterWithConfig/DjangoExporterWithConfig), and known gaps (GORM still has no M2M/junction detection). Updated the stale "66 snapshot files" figure and noted the shared-vs-module-local snapshot suite split.
…n main) CI coverage (--fail-under 100) has been red on main since the composite-FK and M2M work landed, because I only verified test/clippy/fmt/line-budget locally and deferred coverage confirmation to CI without circling back when later pushes showed "coverage" job failures (which I'd wrongly assumed were the already-known codecov-token issue from earlier in the session, without checking each one). Reproduced the exact CI coverage run locally via the xd009642/tarpaulin Docker image and confirmed 4 genuine (non gap-region-artifact) gaps, all now covered: - django/render.rs: a composite-PK junction table whose FKs point at two *other* tables (unrelated to the table being rendered) was never exercised — added a schema with such a table. - django/render.rs: unique_name()'s incrementing-suffix branch (second+ collision) was never exercised — added a direct unit test. - gorm/mod.rs: the equivalent double-collision branch for composite-FK relation field naming was never exercised — added a table with two pre-colliding column names. - gorm/mod.rs: on_update was never set on a composite FK test fixture, so the OnUpdate constraint-tag branch was never hit. Verified via `cargo tarpaulin --engine llvm -p vespertide-exporter` in the same container CI uses: django/render.rs 279/279, gorm/mod.rs 443/443.
The write_futures closure's "create parent dir" if-let block showed as an LLVM gap-region artifact (closing brace + next statement 0-hit despite executing). More importantly, the actual reason it surfaced: render_export_entity's new Orm::Django/Orm::Gorm match arms had no end-to-end test at all — every existing export test only exercised --orm seaorm, so the arms added for the DjangoExporterWithConfig/ GormExporterWithConfig wiring were never hit. - Extracted the parent-dir-creation logic to an `ensure_parent_dir` helper, which incidentally also resolves the gap-region artifact by changing the closure's control-flow shape. - Added end-to-end integration tests that run `export --orm django` and `export --orm gorm` against a real temp project and assert the generated file exists with the expected content. Verified 100.00% (12234/12234 lines) by reproducing the exact CI coverage recipe locally in the xd009642/tarpaulin container: the CI-only wide-width `.rustfmt.toml` + `cargo fmt` reformat pass (which measurably changes LLVM's coverage region boundaries — skipping it produces dozens of false gaps in unrelated files) plus RUST_TEST_THREADS=1 and PROPTEST_CASES=1024 to match the coverage job's determinism settings exactly.
…ryKey
Re-auditing Django against the other 5 backends (specifically asked to
re-check for gaps) surfaced a real correctness bug: for a composite-PK
table, no field was ever marked primary_key=True (only handled for the
single-column case), and Meta declared nothing either. Django's actual
behavior when no field declares primary_key=True is to silently add its
own implicit auto `id` AutoField as the PK — which doesn't correspond to
any real uniqueness constraint on the underlying table, so every
composite-PK schema this exporter has ever produced generated a Django
model whose primary key semantics didn't match the database at all.
GORM and SeaORM both already representing composite PKs correctly
(multiple primaryKey tags / multiple #[sea_orm(primary_key)] columns)
made the Django gap obvious by comparison.
Fixed using Django 5.2+'s native `pk = models.CompositePrimaryKey(...)`,
referencing each column by its Django attname. For FK columns this is
`{field_name}_id` (Django always uses this pattern regardless of any
db_column override) rather than the stripped field name used for the
ForeignKey attribute itself — covered by a dedicated test since it's the
one non-obvious part of the mapping.
Verified 100% coverage of the new code path via the same Docker
tarpaulin reproduction used for the earlier coverage-gap fixes.
|
@owjs3901 리뷰부탁드립니다! |
owjs3901
left a comment
There was a problem hiding this comment.
CICD파일을 변경할 이유가 없습니다
.idea 폴더를 추가할 이유가 없습니다
| @Entity | ||
| @Table(name = "users") | ||
| public class Users { | ||
|
|
||
| @Id | ||
| @Column(name = "id") | ||
| private Integer id; | ||
|
|
||
| @Column(name = "display_name", columnDefinition = "TEXT") | ||
| private String displayName; |
There was a problem hiding this comment.
django와는 무관한 변경사항이 PR에 올라온 것 같습니다
There was a problem hiding this comment.
확인해보니 이 스냅샷은 원래 5월에 삭제됐던 파일인데, 그 사이 upstream 머지로 JPA 테스트 픽스처가 test -> users로 바뀌면서 스냅샷이 없는 깨진 상태였습니다.
Django 작업 중 cargo insta accept를 돌리면서 이 누락분도 같이 재생성돼서 커밋에 껴 들어간 것 같습니다.
Django와는 무관하지만 삭제하면 기존 JPA 테스트가 깨지니, 이 파일은 유지하고 별도 커밋(스냅샷 픽스)으로 분리해도 될까요 ?
Address PR review feedback: no reason to touch CI.yml or add IDE project files in this PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…roSection 컨테이너를 Flex 대신 VStack으로 전환
… 텍스트 공백 제거, eyebrow typography 적용)
| /// Go package name emitted at the top of every generated file | ||
| /// (`package <name>`). Default: `"models"`. | ||
| #[serde(default = "default_gorm_package_name")] | ||
| pub package_name: String, |
There was a problem hiding this comment.
네! 필요합니다.
Go는 파일마다 package 선언이 문법적으로 필수라서 이 값이 있어야 컴파일 가능한 코드가 나옵니다.
실제로 GormExporterWithConfig가 이 값을 읽어서 매 파일 상단에 씁니다 (gorm/mod.rs).
또한 SeaOrmConfig.extra_model_derives, DjangoConfig.app_label처럼 다른 ORM 백엔드도 각자 커스터마이징 옵션을 갖고 있어서, GORM만 없으면 패턴이 깨집니다.
기본값이 "models"라 안 건드리면 기존과 동일하게 동작하고, 프로젝트 구조에 맞게 패키지명을 바꾸고 싶은 사용자를 위한 옵션입니다.
There was a problem hiding this comment.
기본값이 models라면 공통 config의 model_dir 등으로 대체가 가능한지 궁금합니다
혹은 폴더 구조를 통하여 이를 추론할 수 있을지 궁금합니다.
There was a problem hiding this comment.
기본 값이 default_gorm_package_name 로 되어 있는 것도 수정이 필요해보입니다.
| /// label from the containing package. `None` (default) omits | ||
| /// `app_label` and leaves Django's normal inference in place. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub app_label: Option<String>, |
There was a problem hiding this comment.
네! 필요합니다.
Django 자체는 앱이 INSTALLED_APPS에 정상 등록돼 있으면 app_label을 자동 추론하지만, vespertide는 표준 Django 앱 구조 밖에서 모델을 생성하기 때문에 이 추론이 실패하는 경우가 있습니다.
실제 Django 프로젝트에서도 이럴 때 Meta.app_label을 수동 지정하는 게 일반적인 해법이라, 그걸 설정으로 노출한 것입니다.
기본값은 None이라 설정 안 하면 Meta에 아예 안 찍히고(JSON에도 안 남음) Django의 기존 추론 동작 그대로 유지되며, DjangoExporterWithConfig -> render.rs를 거쳐 실제로 끝까지 연결돼 테스트도 커버돼 있습니다.
There was a problem hiding this comment.
폴더 이름 등 환경으로 이를 판단하는 것이 가능한지 궁금합니다, 혹은 파일에 명시되어 있으면 이를 읽고 추론하는 것도 가능해보입니다.
| #[serde(default)] | ||
| pub django: DjangoConfig, | ||
| /// GORM-specific export configuration. | ||
| #[serde(default)] | ||
| pub gorm: GormConfig, |
There was a problem hiding this comment.
최대한 각 orm에 config를 제거할 수 있으면 제거해야 합니다
There was a problem hiding this comment.
좋은 의견 감사합니다!
저도 config는 최대한 가볍게 유지하는 게 좋다고 생각해요.
한번 살펴봤는데 app_label이랑 package_name은 각각 Go 문법상 꼭 있어야 하는 값이랑, Django 앱 구조 추론이 안 되는 경우를 위한 값이라 지금은 빼기가 조금 어려울 것 같아요.
혹시 이 중에 특별히 염두에 두신 필드 있으시면 말씀해주세요! 더 정리해보겠습니다!
| /// a matching variant is added here — a compile-time forcing function that replaces the | ||
| /// old pattern of a runtime `unreachable!()` guard that only a test could catch. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum SimpleColumnKind { |
There was a problem hiding this comment.
동일한 것 같은데 또 선언해야 하는 이유가 있나요
There was a problem hiding this comment.
SimpleColumnType은 #[non_exhaustive]라서 크레이트 밖(exporter 쪽)에서 매치할 때마다 죽은 코드인 와일드카드 arm을 강제로 넣어야 하는데, 그게 커버리지 100% 정책에서 계속 0-hit로 잡힙니다.
SimpleColumnKind는 그 제약이 없는 exhaustive 미러라서 exporter, 쪽(django/types.rs, gorm/mod.rs)에서 와일드카드 없이 완전히 매치할 수 있고, 나중에 타입이 추가되면 From 변환이 컴파일 에러로 알려줘서 놓치는 걸 방지해줍니다!
There was a problem hiding this comment.
아마 버전을 고정하거나 로컬로 고정하거나 하면 같은 코드베이스로 인지하고 이를 해결하는 걸로 알고 있는데.. 이거 rust옵션이 있던걸로 기억합니다, 한번 확인바랍니다!
| class PriorityLevel(models.IntegerChoices): | ||
| LOW = 0, "low" | ||
| MEDIUM = 10, "medium" | ||
| HIGH = 20, "high" |
There was a problem hiding this comment.
확인해봤는데 문법 맞습니다!
Django의 IntegerChoices는 (value, label) 형태를 지원하며, 공식 문서에서도 NO = 0, ("No")와 같은 패턴을 사용하고 있습니다.
LOW = 0, "low"는 괄호를 생략한 튜플 표현으로 (0, "low")와 동일하게 해석됩니다.
enum 값은 반드시 연속적일 필요가 없어서 0, 10, 20으로 지정해도 문제없어 보입니다.
| class OrderStatus(models.TextChoices): | ||
| PENDING = "pending", "pending" | ||
| SHIPPED = "shipped", "shipped" | ||
| DELIVERED = "delivered", "delivered" |
There was a problem hiding this comment.
IntegerChoices 케이스랑 동일한 원리로, "pending", "pending"은 괄호 없는 튜플 패킹이라 ("pending", "pending")으로 해석되고 Django TextChoices가 이 값, 라벨 형태를 그대로 지원합니다. 다만 지금 값이랑 라벨이 똑같은 문자열이라 좀 어색해 보일 수는 있습니다.
원본 enum 값을 그대로 라벨에도 채우고 있어서 그런 건데, 혹시 라벨을 더 사람이 읽기 좋은 형태(예: Title Case)로 바꾸는 게 좋을까요?
| #[cfg(test)] | ||
| pub(crate) fn to_pascal_case_for_tests(s: &str) -> String { | ||
| render::to_pascal_case(s) | ||
| } |
There was a problem hiding this comment.
test 모듈 안에 있어야 합니다 안티패턴입니다
|
충돌 해결이 필요합니다. |
요약
Django, GORM exporter를 검토해서 발견한 부족한 점 7가지를 전부 구현했습니다.
관계(FK) 코드생성이 두 백엔드에서 부분적으로만 지원되고 있었고, 공유 테스트
스위트에도 편입돼 있지 않았습니다. 이번 작업으로 두 백엔드를 나머지 4개
ORM(SeaORM/SQLAlchemy/SQLModel/JPA)과 동등한 수준으로 끌어올렸습니다.
변경 사항
감지해서
ManyToManyField(..., through=...)를 양쪽에 생성. 기존에는스키마 컨텍스트를 아예 무시하고 있었음.
(
foreignKey:...;references:...), Django는 네이티브 지원이 없어서 주석으로관계 정보를 남기도록 처리 (기존엔 컬럼만 남고 관계 정보가 조용히 사라짐).
역방향(has-many) 관계가 아예 생성되지 않던 버그 발견 및 수정.
공유 테스트로 교차검증되고 있었음. 편입 과정에서 Django의 실제 버그 2개를
추가로 발견:
출력되던 문제 (import 시점에 크래시)
primary_key=True가 누락되던 문제 (Django자체 시스템 체크(
fields.E100)에 걸림 — auto PK를 쓰는 거의 모든스키마에 영향)
vespertide.json에서Django
app_label, GORMpackage_name을 커스터마이징할 수 있도록 지원(기존엔 SeaORM만 이런 설정 진입점이 있었음).
clean_export_dir회귀 테스트 추가 — 다른 ORM들은 다 있었는데Django만 빠져 있었음.
머지된 지 오래됐는데도 문서에 전혀 언급이 안 되고 있었음).
다른 5개 백엔드와 다시 비교 검토하다 발견. 복합 PK 테이블에서 어떤
필드에도
primary_key=True가 안 붙고Meta에도 아무 표시가 없어서,Django가 자체적으로 엉뚱한 auto
idPK를 암묵적으로 추가해버리는문제였음 (실제 DB의 PK와 전혀 안 맞음). GORM/SeaORM은 둘 다 이미 제대로
처리하고 있어서 비교하다 바로 드러남. Django 5.2+ 의 네이티브
pk = models.CompositePrimaryKey(...)로 수정.커버리지
위 작업 도중 실제 coverage 회귀(99.92%)가 발생한 걸 CI 실패로 확인하고,
Docker로 CI와 동일한 환경(특수 rustfmt 설정 +
RUST_TEST_THREADS=1+PROPTEST_CASES=1024)을 재현해서 정밀 진단 후 전부 수정했습니다.최종적으로 로컬 재현 환경에서 100.00% (12234/12234 lines) 확인.
검증
cargo test --workspace전체 통과cargo clippy --workspace -- -D warnings클린cargo fmt --all --check클린scripts/check-line-budget.sh통과cargo tarpaulin --engine llvm --fail-under 100— 100% (Docker로 CI 환경동일 재현하여 확인)
CODECOV_TOKEN미설정 이슈로 실패 — 코드와 무관)