Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9d1618c
chore: 불필요 파일 제거
opficdev Aug 19, 2026
857ceec
chore: tuist에 맞도록 CollectionUI 구성
opficdev Aug 20, 2026
90d7aab
feat: CollectionUI rendering snapshot 추가
opficdev Aug 20, 2026
1b9a2f3
feat: CollectionUI diffable controller 추가
opficdev Aug 20, 2026
4de2fbc
feat: CollectionUI SwiftUI 연결 추가
opficdev Aug 20, 2026
ce233dc
feat: CollectionUI event callback 추가
opficdev Aug 20, 2026
6739716
feat: CollectionUI swipe action 추가
opficdev Aug 20, 2026
dd6cc47
feat: CollectionUI refresh 추가
opficdev Aug 20, 2026
780a665
feat: CollectionUI supplementary view 추가
opficdev Aug 20, 2026
9ae0844
feat: CollectionUI lifecycle 정리 추가
opficdev Aug 20, 2026
6f589dc
docs: CollectionUI 사용 예시 추가
opficdev Aug 20, 2026
a446a24
feat: CollectionUI 상태 복원 처리 추가
opficdev Aug 20, 2026
b896f81
test: CollectionUI interaction 검증 추가
opficdev Aug 20, 2026
4a85f5b
test: CollectionUI 재구성 snapshot 검증 수정
opficdev Aug 20, 2026
86f1016
style: CollectionUI 주석 문체 정리
opficdev Aug 20, 2026
2d53e4a
refactor: 불필요 import 제거
opficdev Aug 20, 2026
6d9e889
style: 방향 주석 수정
opficdev Aug 20, 2026
6167257
docs: 의존성 이미지 업데이트
opficdev Aug 20, 2026
8a043e0
fix: CollectionUI 재구성 요청 유지
opficdev Aug 20, 2026
61cec11
fix: CollectionUI 거부 snapshot 유지
opficdev Aug 20, 2026
05bf68f
fix: CollectionUI layout 갱신 추가
opficdev Aug 20, 2026
17d28fc
fix: CollectionUI 테스트 lint 설정 분리
opficdev Aug 20, 2026
48a800e
fix: CollectionUI refresh control 조건 처리
opficdev Aug 20, 2026
f015463
fix: CollectionUI 사용자 조작 보존
opficdev Aug 20, 2026
ef42e79
feat: CollectionUI section 재구성 추가
opficdev Aug 20, 2026
fcd59ea
ci: CollectionUI 검증 추가
opficdev Aug 20, 2026
b340b9b
chore: Spark 역할 fallback 추가
opficdev Aug 20, 2026
7cbc3ed
ci: CollectionUITests 실행 수정
opficdev Aug 20, 2026
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
10 changes: 7 additions & 3 deletions .agents/roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Use these model tiers when assigning work to another LLM.
| Tier | Use | Default model |
| --- | --- | --- |
| `Primary` | Planning, implementation, architecture decisions, final integration, failed-check triage | Strongest available Codex/GPT coding model |
| `Lightweight` | Read-only review, checklist validation, log summarization, documentation draft, first-pass architecture preflight | Pinned non-Primary model from the configured custom agent TOML |
| `Lightweight` | Read-only review, checklist validation, log summarization, documentation draft, first-pass architecture preflight | `gpt-5.3-codex-spark`, unavailable 시 `gpt-5.6-luna`와 `high` 추론 |
| `Fast` | Low-risk text cleanup, simple file presence checks, short summaries | Pinned fast model from the configured custom agent TOML when a Fast role is defined |

Default role-to-model and execution assignment:
Expand Down Expand Up @@ -55,7 +55,8 @@ Do not assign `Lightweight` as the only model for production Swift implementatio
- A sub-agent that inherits the active `Primary` model does not satisfy a `Lightweight` or `Fast` assignment.
- Do not satisfy a `Lightweight` or `Fast` role by completing the role directly in `Primary` and describing it as delegated work.
- A generic sub-agent spawn that does not load the configured custom agent TOML does not satisfy the role assignment.
- If the custom agent cannot be loaded, its pinned model is unavailable, or the dispatch surface cannot select that custom agent, stop before dispatch and report which role cannot run.
- If the custom agent cannot be loaded or the dispatch surface cannot select that custom agent, stop before dispatch and report which role cannot run.
- A configured `gpt-5.3-codex-spark` model is unavailable only when the connected side-task surface cannot select it after an exact `task_name` retry. In that case, dispatch the matching `*_luna` custom role with `gpt-5.6-luna` and `high` reasoning effort. Do not select another fallback model.
- If the assigned model is available but current tool policy requires explicit user permission before dispatch, missing permission is not fallback. Stop and ask for permission before continuing the required role.
- `Primary` must integrate and verify delegated output, but must not skip the delegated role when the workflow requires it and the assigned model is available.

Expand All @@ -81,10 +82,13 @@ Use these exact role identifiers:
| GitHub/CI Analyst | `github_ci_analyst` | `.codex/agents/github_ci_analyst.toml` |
| Documentation Writer | `documentation_writer` | `.codex/agents/documentation_writer.toml` |

Spark fallback custom agents use the same role suffix with `_luna`: `architecture_watcher_luna`, `code_reviewer_luna`, `verification_runner_luna`, `github_ci_analyst_luna`, and `documentation_writer_luna`.

### Fallback policy

- The configured custom agent TOML is the source of truth for the non-Primary role model and sandbox.
- If a required custom agent or its pinned non-Primary model is unavailable, do not fall back to another model; stop and report the unavailable role.
- The configured custom agent TOML keeps `gpt-5.3-codex-spark` as the default non-Primary model. If Spark is unavailable, use only the matching `*_luna` custom agent with `gpt-5.6-luna` and `high` reasoning effort, preserving the same sandbox and developer instructions.
- If `gpt-5.6-luna` with `high` reasoning effort is also unavailable, do not fall back to another model; stop and report the unavailable role.
- If `Primary` is unavailable, do not perform implementation, architecture verdict, final integration, git write actions, or GitHub write actions.
- Do not downgrade `Primary` roles to `Lightweight` or `Fast` only because a cheaper model is available.
- For user-facing summaries, a lower tier may draft text, but `Primary` must check it when the text depends on architecture decisions, release risk, CI root cause, or exact diff behavior.
Expand Down
2 changes: 1 addition & 1 deletion .agents/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Stop and ask the user before editing when:
- The task packet conflicts with `AGENTS.md`.
- The requested fix requires relaxing a layer boundary.
- A role needs to run, launch, install, boot, or open the app or Simulator.
- A required `Lightweight` or `Fast` custom agent cannot be loaded or selected through the connected side-task surface with its exact `task_name`, its pinned model is unavailable, or current tool policy requires user permission that has not been granted.
- A required `Lightweight` or `Fast` custom agent cannot be loaded or selected through the connected side-task surface with its exact `task_name`, the matching `*_luna` fallback cannot be selected after Spark is unavailable, or current tool policy requires user permission that has not been granted.
- The current issue or PR scope is unclear after live GitHub inspection.
- Two editing roles would touch the same file.
- A read-only role reports `Block` or `Needs Owner Decision`.
Expand Down
12 changes: 12 additions & 0 deletions .codex/agents/architecture_watcher_luna.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "architecture_watcher_luna"
description = "Fallback DevLog architecture boundary reviewer when gpt-5.3-codex-spark is unavailable."
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
Read AGENTS.md and .agents/roles.md before reviewing.
Act only as the Architecture Watcher defined in .agents/roles.md.
Inspect the requested architecture scope and relevant manifests without editing files or changing GitHub state.
Do not run, launch, install, boot, or open the app or Simulator.
Return exactly the Architecture Watch Result format from .agents/roles.md.
"""
12 changes: 12 additions & 0 deletions .codex/agents/code_reviewer_luna.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "code_reviewer_luna"
description = "Fallback DevLog code reviewer when gpt-5.3-codex-spark is unavailable."
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
Read AGENTS.md and .agents/roles.md before reviewing.
Act only as the Code Reviewer defined in .agents/roles.md.
Review the assigned diff without editing files, staging changes, committing, pushing, or changing GitHub state.
Prioritize correctness, regressions, architecture drift, scope drift, and missing tests over style preferences.
Return exactly the Code Review Result format from .agents/roles.md with file and line references when possible.
"""
12 changes: 12 additions & 0 deletions .codex/agents/documentation_writer_luna.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "documentation_writer_luna"
description = "Fallback DevLog documentation writer when gpt-5.3-codex-spark is unavailable."
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
Read AGENTS.md and .agents/roles.md before drafting.
Act only as the Documentation Writer defined in .agents/roles.md.
Match repository templates, actual diffs, live issue or PR state, and the requested Korean wording rules.
Edit only documentation files named in the task packet. Do not edit app code or create GitHub content unless the task packet explicitly authorizes that write action.
Return exactly the Documentation Result format from .agents/roles.md unless the task packet requests direct Markdown draft output.
"""
12 changes: 12 additions & 0 deletions .codex/agents/github_ci_analyst_luna.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name = "github_ci_analyst_luna"
description = "Fallback DevLog GitHub and CI analyst when gpt-5.3-codex-spark is unavailable."
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
Read AGENTS.md and .agents/roles.md before analysis.
Act only as the GitHub/CI Analyst defined in .agents/roles.md.
Use live GitHub state as the source of truth and use thread-aware review inspection when resolution state matters.
Do not edit files, reply, resolve threads, submit reviews, push, or change GitHub state unless the task packet explicitly authorizes that write action.
Return exactly the GitHub CI Result format from .agents/roles.md.
"""
13 changes: 13 additions & 0 deletions .codex/agents/verification_runner_luna.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name = "verification_runner_luna"
description = "Fallback DevLog verification runner when gpt-5.3-codex-spark is unavailable."
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
Read AGENTS.md and .agents/roles.md before verification.
Act only as the Verification Runner defined in .agents/roles.md.
Do not edit source or documentation files, except through an explicitly assigned formatting command.
Do not run, launch, install, boot, or open the app or Simulator.
Run only the checks assigned in the task packet and record exact commands, exit status, and evidence.
Return exactly the Verification Result format from .agents/roles.md.
"""
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ jobs:
- name: Persistence
schemes: "Persistence"
- name: Libraries
schemes: "MarkdownRenderer"
schemes: "MarkdownRenderer CollectionUITests"
- name: Presentation
schemes: "Entry PresentationShared HomeTab"
- name: Widget
Expand Down Expand Up @@ -429,6 +429,8 @@ jobs:
LOG_PATH="$RESULT_DIR/${TEST_SCHEME}.log"
if [ "$TEST_SCHEME" = "WidgetCore" ]; then
TEST_SOURCE_DIR="Widget/WidgetCore/Tests"
elif [ "$TEST_SCHEME" = "CollectionUITests" ]; then
TEST_SOURCE_DIR="Libraries/CollectionUI/Tests"
elif [ -d "Libraries/${TEST_SCHEME}/Tests" ]; then
TEST_SOURCE_DIR="Libraries/${TEST_SCHEME}/Tests"
elif [ -d "Application/Presentation/${TEST_SCHEME}/Tests" ]; then
Expand Down
16 changes: 0 additions & 16 deletions Application/App/Sources/App/TempView.swift

This file was deleted.

69 changes: 69 additions & 0 deletions Libraries/CollectionUI/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import ProjectDescription
import ProjectDescriptionHelpers

let deploymentSettings: SettingsDictionary = [
"IPHONEOS_DEPLOYMENT_TARGET": "17.0",
"MARKETING_VERSION": "1.0.0",
]

let project = Project(
name: "CollectionUI",
packages: [],
settings: .devlogProject(additionalBase: deploymentSettings),
targets: [
.target(
name: "CollectionUI",
destinations: .iOS,
product: .framework,
bundleId: "com.opfic.DevLog.CollectionUI",
infoPlist: .extendingDefault(
with: [
"CFBundlePackageType": "FMWK",
]
),
sources: [
"Sources/**/*.swift",
],
scripts: [
DevLogScripts.swiftLint(
sourcePath: "Sources",
configPath: "Sources/.swiftlint.yml"
),
],
dependencies: [],
settings: .devlog(
base: deploymentSettings
)
),
.target(
name: "CollectionUITests",
destinations: .iOS,
product: .unitTests,
bundleId: "com.opfic.DevLog.CollectionUITests",
infoPlist: .extendingDefault(
with: [
"CFBundlePackageType": "BNDL",
]
),
sources: [
"Tests/**/*.swift",
],
scripts: [
DevLogScripts.swiftLint(
sourcePath: "Tests",
configPath: "Tests/.swiftlint.yml"
),
],
dependencies: [
.target(name: "CollectionUI"),
],
settings: .devlog(
base: deploymentSettings.merging(
[
"TEST_TARGET_NAME": "CollectionUI",
]
) { _, new in new }
)
),
]
)
30 changes: 30 additions & 0 deletions Libraries/CollectionUI/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# CollectionUI

`CollectionUI`는 SwiftUI 화면에서 UIKit `UICollectionView`를 연결하는 내부 공용 Library임.

```swift
CollectionView(
snapshot: .init(sections: [
.init(identifier: .default, itemIdentifiers: todos.map(\.id))
]),
layoutProvider: { _, _ in makeTodoLayout() },
cellProvider: { collectionView, indexPath, identifier in
collectionView.dequeueConfiguredReusableCell(
using: todoCellRegistration,
for: indexPath,
item: identifier
)
}
)
.onSelect { identifier in
store.send(.view(.select(identifier)))
}
.swipeActions(edge: .trailing) { identifier in
[deleteAction(identifier)]
}
.refreshable {
await store.send(.view(.refresh)).finish()
}
```

Cell은 `UICollectionViewCell`과 UIKit `UIView`로만 구성하며, `UIHostingController`, `UIHostingConfiguration`, `AnyView`는 사용하지 않음.
1 change: 1 addition & 0 deletions Libraries/CollectionUI/Sources/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
parent_config: ../../../.swiftlint.yml
47 changes: 47 additions & 0 deletions Libraries/CollectionUI/Sources/CollectionRenderingSnapshot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// CollectionRenderingSnapshot.swift
// CollectionUI
//
// Created by opfic on 8/20/26.
//

import Foundation

// Collection 화면에 적용할 section과 item 식별자 상태
public struct CollectionRenderingSnapshot<SectionIdentifier, ItemIdentifier>: Equatable, Sendable
where SectionIdentifier: Hashable & Sendable, ItemIdentifier: Hashable & Sendable {
// 하나의 section과 그 안의 item 식별자 순서
public struct Section: Equatable, Sendable {
// section을 구분하는 고유 식별자
public let identifier: SectionIdentifier
// section 안에 표시할 item의 순서가 있는 식별자 목록
public let itemIdentifiers: [ItemIdentifier]

// section 식별자와 표시 순서를 지정한 section 값 생성
public init(
identifier: SectionIdentifier,
itemIdentifiers: [ItemIdentifier]
) {
self.identifier = identifier
self.itemIdentifiers = itemIdentifiers
}
}

// 표시할 section과 item 식별자의 전체 순서
public let sections: [Section]
// 같은 식별자를 유지한 채 다시 구성할 item 식별자 집합
public let reconfiguredItemIdentifiers: Set<ItemIdentifier>
// 같은 식별자를 유지한 채 다시 구성할 section 식별자 집합
public let reconfiguredSectionIdentifiers: Set<SectionIdentifier>

// section 순서와 다시 구성할 item 식별자를 지정한 rendering snapshot 생성
public init(
sections: [Section],
reconfiguredItemIdentifiers: Set<ItemIdentifier> = [],
reconfiguredSectionIdentifiers: Set<SectionIdentifier> = []
) {
self.sections = sections
self.reconfiguredItemIdentifiers = reconfiguredItemIdentifiers
self.reconfiguredSectionIdentifiers = reconfiguredSectionIdentifiers
}
}
15 changes: 15 additions & 0 deletions Libraries/CollectionUI/Sources/CollectionSwipeActions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import UIKit

// UIKit swipe action을 적용할 방향
public enum CollectionSwipeActionsEdge: Equatable, Sendable {
// cell의 좌측에서 우측 방향 swipe action
case leading
// cell의 우측에서 좌측 방향 swipe action
case trailing
}

struct CollectionSwipeActions<ItemIdentifier> where ItemIdentifier: Hashable & Sendable {
let edge: CollectionSwipeActionsEdge
let allowsFullSwipe: Bool
let actions: (ItemIdentifier) -> [UIContextualAction]
}
39 changes: 39 additions & 0 deletions Libraries/CollectionUI/Sources/CollectionView+Events.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import SwiftUI
import UIKit

public extension CollectionView {
// item 선택 시 전달할 동작 지정
func onSelect(_ action: @escaping (ItemIdentifier) -> Void) -> Self {
var view = self
view.configuration.onSelect = action
return view
}

// item이 표시되기 시작할 때 전달할 동작 지정
func onWillDisplay(_ action: @escaping (ItemIdentifier) -> Void) -> Self {
var view = self
view.configuration.onWillDisplay = action
return view
}

// item 선준비를 시작할 때 전달할 동작 지정
func onPrefetch(_ action: @escaping ([ItemIdentifier]) -> Void) -> Self {
var view = self
view.configuration.onPrefetch = action
return view
}

// item 선준비를 취소할 때 전달할 동작 지정
func onCancelPrefetch(_ action: @escaping ([ItemIdentifier]) -> Void) -> Self {
var view = self
view.configuration.onCancelPrefetch = action
return view
}

// scroll 위치가 바뀔 때 전달할 동작 지정
func onScroll(_ action: @escaping (CGPoint) -> Void) -> Self {
var view = self
view.configuration.onScroll = action
return view
}
}
10 changes: 10 additions & 0 deletions Libraries/CollectionUI/Sources/CollectionView+Refresh.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SwiftUI

public extension CollectionView {
// UIKit 새로고침 완료를 기다릴 비동기 동작 지정
func refreshable(_ action: @escaping () async -> Void) -> Self {
var view = self
view.configuration.refreshAction = action
return view
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SwiftUI
import UIKit

public extension CollectionView {
// section 식별자로 supplementary view를 만들 동작 지정
func supplementaryViews(
_ provider: @escaping (UICollectionView, String, IndexPath, SectionIdentifier) -> UICollectionReusableView?
) -> Self {
var view = self
view.configuration.supplementaryViewProvider = provider
return view
}
}
19 changes: 19 additions & 0 deletions Libraries/CollectionUI/Sources/CollectionView+SwipeActions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SwiftUI
import UIKit

public extension CollectionView {
// item 식별자로 UIKit swipe action을 만들 동작 추가
func swipeActions(
edge: CollectionSwipeActionsEdge = .trailing,
allowsFullSwipe: Bool = true,
actions: @escaping (ItemIdentifier) -> [UIContextualAction]
) -> Self {
var view = self
view.configuration.swipeActions.append(.init(
edge: edge,
allowsFullSwipe: allowsFullSwipe,
actions: actions
))
return view
}
}
Loading
Loading