{"id":32390437,"url":"https://github.com/teddko/tedd_transformation","last_synced_at":"2026-07-16T07:32:36.539Z","repository":{"id":319588815,"uuid":"1075379506","full_name":"TeddKo/tedd_transformation","owner":"TeddKo","description":null,"archived":false,"fork":false,"pushed_at":"2025-10-19T13:34:56.000Z","size":111,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-19T16:46:42.021Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TeddKo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-13T12:23:46.000Z","updated_at":"2025-10-19T13:34:59.000Z","dependencies_parsed_at":null,"dependency_job_id":"85cf7a0d-fe26-407a-b0fa-1c645a82d6a2","html_url":"https://github.com/TeddKo/tedd_transformation","commit_stats":null,"previous_names":["teddko/tedd_transformation"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/TeddKo/tedd_transformation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeddKo%2Ftedd_transformation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeddKo%2Ftedd_transformation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeddKo%2Ftedd_transformation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeddKo%2Ftedd_transformation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TeddKo","download_url":"https://codeload.github.com/TeddKo/tedd_transformation/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeddKo%2Ftedd_transformation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35535880,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-16T02:00:06.687Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-25T04:49:29.820Z","updated_at":"2026-07-16T07:32:36.521Z","avatar_url":"https://github.com/TeddKo.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tedd_transformation\n\n## 문서 목차\n\n- [주요 기능](#주요-기능)\n- [빠른 시작](#빠른-시작)\n- [프로젝트 구조](#프로젝트-구조)\n- [계층 구조](#계층-구조)\n- [핵심 컴포넌트 상세](#핵심-컴포넌트-상세)\n  - [CoordinateSystem](#1-좌표-시스템-coordinatesystemkt)\n  - [TransformCalculator](#2-변환-계산기-transformcalculatorkt)\n  - [CanvasDrawing](#2-1-캔버스-드로잉-canvasdrawingkt)\n- [UI 컴포넌트](#4-ui-컴포넌트)\n- [상태 관리](#상태-관리-단일-컨트롤러-상태)\n- [개념 정리(좌표계/Position/Pivot)](#-개념-정리-position-vs-pivot-좌표계)\n- [입력 규칙 및 폴백](#입력-규칙-및-폴백-동작)\n- [동작 원리 흐름도](#동작-원리)\n- [MVI 아키텍처](#mvi-아키텍처)\n\n### 주요 기능\n\n- **실시간 좌표 변환**: Position, Rotation, Pivot 파라미터 입력\n- **시각적 그리드 시스템**: −250 ~ 250 범위의 좌표계 (50단위 그리드)\n- **정점 좌표 계산**: 변환 후 사각형 네 꼭짓점의 정확한 좌표 표시\n- **피벗 기반 회전**: 임의의 피벗 점을 중심으로 회전\n\n### 프로젝트 구조\n\n```mermaid\nflowchart TD\n    A[app/] --\u003e A1[src/]\n    A1 --\u003e A2[main/]\n    A2 --\u003e AK[kotlin/]\n    A2 --\u003e AR[res/]\n\n    %% Kotlin 소스 트리\n    AK --\u003e C1[com/]\n    C1 --\u003e C2[tedd/]\n    C2 --\u003e ROOT[com.tedd.tedd_transform/]\n\n    %% 루트 파일 및 디렉터리\n    ROOT --\u003e M0[MainActivity.kt]\n    ROOT --\u003e CONST[constants/]\n    ROOT --\u003e EXT[extensions/]\n    ROOT --\u003e MODELS[models/]\n    ROOT --\u003e UI[ui/]\n    ROOT --\u003e UTILS[utils/]\n\n    %% constants\n    CONST --\u003e CONST_F[DrawingConstants.kt]\n\n    %% extensions\n    EXT --\u003e EXT_F[OffsetExtensions.kt]\n\n    %% models\n    MODELS --\u003e MODELS_F[TransformModels.kt]\n\n    %% ui\n    UI --\u003e UI_MAIN[TransformControllerScreen.kt]\n    UI --\u003e UI_STATE[state/]\n    UI --\u003e UI_CANVAS[canvas/]\n    UI --\u003e UI_COMP[components/]\n    UI --\u003e UI_THEME[theme/]\n    UI_STATE --\u003e UI_S1[RememberTransformControllerState.kt]\n    UI_CANVAS --\u003e UI_V1[CanvasDrawing.kt]\n    UI_COMP --\u003e UI_C1[DrawingArea.kt]\n    UI_COMP --\u003e UI_C2[TransformControls.kt]\n    UI_COMP --\u003e UI_C3[VertexCoordinatesDisplay.kt]\n    UI_THEME --\u003e UI_T1[Color.kt]\n    UI_THEME --\u003e UI_T2[Theme.kt]\n    UI_THEME --\u003e UI_T3[Type.kt]\n\n    %% utils\n    UTILS --\u003e U1[CoordinateSystem.kt]\n    UTILS --\u003e U2[TransformCalculator.kt]\n\n    %% res 하위 (요약)\n    AR --\u003e RV[values/]\n    RV --\u003e RV1[strings.xml]\n    RV --\u003e RV2[colors.xml]\n    RV --\u003e RV3[themes.xml]\n```\n\n### 계층 구조\n\n```mermaid\ngraph TD\n    A[MainActivity.kt\u003cbr/\u003e앱 진입점] --\u003e B[TransformControllerScreen.kt\u003cbr/\u003eView]\n    B --\u003e C[DrawingArea\u003cbr/\u003eCanvas 렌더링]\n    B --\u003e D[VertexCoordinatesDisplay\u003cbr/\u003e좌표 표시]\n    B --\u003e E[TransformControls\u003cbr/\u003e입력 컨트롤]\n\n    %% MVI 흐름\n    E -- onChange/onClick --\u003e I[TransformationIntent]\n    I --\u003e R[Reducer\u003cbr/\u003eTransformIntent.intent]\n    R --\u003e S[\"State\u003cbr/\u003eTransformControllerStateHolder\u003cbr/\u003erememberTransformControllerState\"]\n    S --\u003e B\n\n    %% 비즈니스 로직/모델 계층\n    C --\u003e F[Utils \u0026 Models\u003cbr/\u003e비즈니스 로직]\n    D --\u003e F\n    F --\u003e G[CoordinateSystem]\n    F --\u003e H[TransformCalculator]\n    F --\u003e M[TransformModels]\n```\n\n## 핵심 컴포넌트 상세\n\n### 1. 좌표 시스템 (CoordinateSystem.kt)\n\n**역할**: Global 좌표계와 Android Canvas 좌표계 간 변환\n\n```kotlin\nclass CoordinateSystem(canvasWidth, canvasHeight) {\n    // Global 좌표 → Canvas 좌표\n    fun toCanvasX(globalX: Float): Float\n    fun toCanvasY(globalY: Float): Float\n    fun toCanvas(global: Offset): Offset\n}\n```\n\n#### 동작 원리\n\n- 입력 파라미터\n  - `canvasWidth`, `canvasHeight`: Compose `Canvas`의 실제 픽셀 크기. `DrawScope.size`에서 전달됩니다.\n  - 그리드 가이드 범위: `Grid.MIN = -250f`, `Grid.MAX = 250f` → `coordRange = MAX - MIN = 500f`.\n\n- 스케일 계산\n  - `scale = min(canvasWidth, canvasHeight) / coordRange`\n    - 짧은 변을 기준으로 스케일을 잡아 ±250 정사각형 가이드가 화면에 맞게 들어갑니다.\n    - 화면 비율에 따라 상/하 또는 좌/우에 여백이 생길 수 있습니다.\n\n- 좌표 매핑 공식 (Global → Canvas)\n  - X: `x_canvas = (x_global - MIN) * scale`\n  - Y: `y_canvas = canvasHeight - (y_global - MIN) * scale`\n  - 오프셋: `toCanvas(Offset(x, y)) = Offset(x_canvas, y_canvas)`\n\n- 의미/특징\n  - Global 좌표계는 X→오른쪽+, Y→위쪽+이며, 가이드 범위(−250..250) 내에서 원점(0,0)은 중앙에 위치합니다.\n  - 가이드 범위는 −250..250이며, 매핑 자체는 범위를 넘는 좌표에도 그대로 적용되어 그리드 밖으로 그려질 수 있습니다.\n  - 두 축에 동일한 스케일을 사용하므로 비율 왜곡이 없습니다(정사각형 유지).\n\n- 예시\n  - `(MIN, MIN)=(-250, -250)` → `(0, canvasHeight)`\n  - `(0, 0)` → `(250 * scale, canvasHeight - 250 * scale)`\n  - `(MAX, MAX)=(250, 250)` → `(500 * scale, canvasHeight - 500 * scale)`\n\n**좌표계 구분 (Local / Global / Canvas)**\n- **Local 좌표계**: 도형(사각형)의 내부 좌표계. 원점은 사각형의 Left-Bottom(좌하단), X→오른쪽+, Y→위쪽+. 초기 정점은 `(0,0)=LeftBottom`, `(RECT_SIZE,0)=RightBottom`, `(RECT_SIZE,RECT_SIZE)=RightTop`, `(0,RECT_SIZE)=LeftTop` 기준으로 정의됩니다.\n- **Global 좌표계**: X→오른쪽+, Y→위쪽+. 가이드 범위는 −250..250이며, 원점(0,0)은 가이드의 중앙에 위치합니다.\n- **Canvas 좌표계**: Android 픽셀 좌표. 좌상단이 원점, X→오른쪽+, Y→아래쪽+. `CoordinateSystem`이 Global ⇄ Canvas 변환을 담당합니다.\n\n### 2. 변환 계산기 (TransformCalculator.kt)\n\n**역할**: 회전 및 변환 행렬 계산\n\n```kotlin\nobject TransformCalculator {\n    // 변환 후 정점 좌표 계산 (Global)\n    fun calculateVertices(state: TransformState): List\u003cVertex\u003e\n\n    // 변환 후 정점 좌표 계산 (Canvas)\n    fun calculateTransformedVertices(\n        state: TransformState,\n        coordSystem: CoordinateSystem\n    ): List\u003cOffset\u003e\n}\n```\n\n**변환 순서**:\n1. **Local → Pivot 기준 회전**: 피벗 점을 원점으로 이동 → 회전 → 복원\n2. **Translation**: 회전 결과를 `position - pivot` 만큼 이동\n\n**회전 행렬**:\n```\nx' = (x - pivotX) * cos(θ) - (y - pivotY) * sin(θ) + pivotX\ny' = (x - pivotX) * sin(θ) + (y - pivotY) * cos(θ) + pivotY\n```\n\n#### 동작 원리\n\n- 입력값\n  - `state.pivot (p)`: Local 좌표계의 회전 중심점\n  - `state.position (P)`: Pivot의 Global 좌표(빨간 점)\n  - `state.rotation (θ)`: 도 단위 각도(반시계 +); 내부에서 라디안으로 변환하여 `cos/sin` 사용\n\n- 로컬 사각형 정의(좌하단 원점)\n  - 정점 리스트 `v0..v3`는 아래 순서/위치로 생성됩니다.\n    - `v0=(0, RECT_SIZE)` → LeftTop, `v1=(RECT_SIZE, RECT_SIZE)` → RightTop\n    - `v2=(RECT_SIZE, 0)` → RightBottom, `v3=(0, 0)` → LeftBottom\n  - 각 정점에 `VertexType`을 동일한 순서로 매핑합니다.\n\n- 전역 좌표 계산식\n  - 각 로컬 정점 `v`에 대해:\n    - `V_global = R(θ) · (v - p) + P`\n    - 여기서 `R(θ)`는 2×2 회전 행렬, `p=state.pivot`, `P=state.position`\n  - 구현 상 동일식: `rotated = rotateAround(v, p)` → `global = rotated - p + P`\n\n- 메서드별 반환\n  - `calculateVertices(state)`: 위 공식을 통해 계산된 Global 좌표(수학적 Y 위로 +)를 `Vertex(type, x, y)`로 반환\n  - `calculateTransformedVertices(state, coordSystem)`: 위 Global 좌표를 `CoordinateSystem`으로 Canvas 좌표(좌상단 원점, Y 아래로 +)로 변환하여 `Offset` 리스트 반환\n\n- 각도/방향성\n  - θ가 양수이면 수학적 좌표계(우핸드, Y 위로 +) 기준 반시계(CCW) 회전입니다. 현재 Global이 Y 위로 +이므로 직관적 CCW가 됩니다.\n\n- 정밀도/성능\n  - 각 프레임마다 `cos(θ), sin(θ)`를 1회만 계산하여 네 정점에 재사용합니다.\n  - `RECT_SIZE`와 `INTERVAL`이 정수값이지만 계산은 `Float`로 수행합니다(좌표 표시 시 `%.2f`).\n\n### 2-1. 캔버스 드로잉 (CanvasDrawing.kt)\n\n**역할**: Global 좌표를 Canvas 픽셀 좌표로 변환해 격자/축/도형/라벨/피벗을 그립니다. `DrawingArea`에서 순서대로 호출됩니다.\n\n구성 함수와 동작\n- `drawGrid(coordSystem)`\n  - 틱 순회: `tick = MIN..MAX`를 `INTERVAL` 간격의 정수로 순회해 부동소수 오차 없이 0축을 판별합니다.\n  - 축 판별: `isAxis = (tick == 0)`.\n  - 렌더링: x=tick 수직선과 y=tick 수평선을 전 범위에 그립니다.\n    - 축(0): 검정(Black), 굵기 `AXIS_STROKE_WIDTH`\n    - 일반: 회색(Gray), 굵기 `GRID_STROKE_WIDTH`\n  - 좌표 변환: 각 선의 시작/끝 좌표를 `coordSystem.toCanvasX/Y(...)`로 변환합니다.\n\n- `drawAxisLabels(coordSystem)`\n  - 기준축: `x=0`, `y=0`의 캔버스 위치를 먼저 구해 라벨 기준선으로 사용합니다.\n  - 라벨 범위: MIN..MAX를 INTERVAL 간격으로 순회하되 0은 제외(축 겹침 방지).\n  - 배치: X축 라벨은 아래쪽(바닥선) 근처, Y축 라벨은 왼쪽 축 근처에 `LABEL_OFFSET_X/Y` 만큼 오프셋을 두고 `nativeCanvas.drawText`로 그립니다.\n\n- `drawRectangle(state, coordSystem)`\n  - 정점 계산: `TransformCalculator.calculateTransformedVertices(state, coordSystem)`로 Canvas 좌표의 네 꼭짓점을 구합니다.\n  - 그리기: `Path`로 네 점을 연결해 채우기(연한 회색 alpha 0.8) 후 외곽선(검정, `RECT_STROKE_WIDTH`)을 그립니다.\n\n- `drawPivotPoint(state, coordSystem)`\n  - 위치: `state.position`(Pivot의 Global 좌표)을 Canvas로 변환 후 원을 그립니다.\n  - 스타일: 빨간색(Red), 반지름 `PIVOT_RADIUS` 픽셀.\n\n호출 순서와 레이어링\n- `DrawingArea`의 `Canvas` 내부에서 다음 순서로 호출됩니다: 그리드 → 축 라벨 → 사각형 → 피벗 점.\n- 결과적으로 피벗 점이 가장 위에 올라와 항상 보입니다.\n\n### 3. 데이터 모델 (TransformModels.kt)\n\n```kotlin\n// 변환 상태\ndata class TransformState(\n    val position: Offset = Offset.Zero,  // 이동\n    val rotation: Float = 0f,            // 회전 각도 (도)\n    val pivot: Offset = Offset.Zero      // 회전 중심점\n)\n\n// 정점 타입 (Sealed Class)\nsealed class VertexType(@StringRes val stringResId: Int) {\n    data object LeftTop\n    data object RightTop\n    data object RightBottom\n    data object LeftBottom\n}\n\n// 정점 좌표\ndata class Vertex(\n    val type: VertexType,\n    val x: Float,\n    val y: Float\n)\n```\n\n### 4. UI 컴포넌트\n\n#### DrawingArea (Canvas 렌더링)\n- **Grid Lines**: 50단위 그리드 + 축 라인\n- **Axis Labels**: 좌표 값 표시\n- **Rectangle**: 변환된 사각형\n- **Pivot Point**: 빨간 점으로 위치 표시\n\n#### VertexCoordinatesDisplay\n- 네 꼭짓점의 좌표를 실시간 표시- 형식: `Left-Top: (x, y)`\n\n#### TransformControls\n- **Position 입력**: \"x, y\" 형식\n- **Rotation 입력**: 각도 (도)\n- **Pivot 입력**: \"x, y\" 형식\n- **Apply 버튼**: 변환 적용\n\n## 상태 관리\n\n입력 버퍼(draft)와 적용 상태(applied)를 하나로 묶은 `TransformControllerState`를 사용합니다. Compose에서는 `rememberTransformControllerState()` 한 번만 호출하면 됩니다. 내부적으로 `rememberSaveable` + `Saver`를 통해 구성 변경 후에도 상태가 복원됩니다.\n\n### API\n\n```kotlin\n@Composable\nfun rememberTransformControllerState(\n    initial: TransformControllerState = TransformControllerState()\n): TransformControllerStateHolder\n\n@Stable\nclass TransformControllerStateHolder internal constructor(\n  private val state: MutableState\u003cTransformControllerState\u003e\n) {\n  val value: TransformState get() = state.value.applied\n  val draftValue: TransformDraft get() = state.value.draft\n\n  fun updateApplied(block: (TransformState) -\u003e TransformState)\n  fun updateDraft(block: (TransformDraft) -\u003e TransformDraft)\n}\n\ndata class TransformControllerState(\n  val applied: TransformState = TransformState(),\n  val draft: TransformDraft = TransformDraft()\n)\n\ndata class TransformDraft(\n  val position: String = \"\",\n  val rotation: String = \"\",\n  val pivot: String = \"\"\n)\n```\n\n### 사용 예시 (TransformControllerScreen)\n\n```kotlin\nval controller = rememberTransformControllerState()\nval transformIntent = TransformIntent(controller)\n\nDrawingArea(state = controller.value)\nval vertices = TransformCalculator.calculateVertices(controller.value)\n\nTransformControls(\n  position = controller.draftValue.position,\n  rotation = controller.draftValue.rotation,\n  pivot = controller.draftValue.pivot,\n  onPositionChange = { transformIntent.intent(TransformationIntent.InputPosition(it)) },\n  onRotationChange = { transformIntent.intent(TransformationIntent.InputRotation(it)) },\n  onPivotChange = { transformIntent.intent(TransformationIntent.InputPivot(it)) },\n  onApply = { transformIntent.intent(TransformationIntent.Apply) }\n)\n```\n\n### 저장/복원 동작\n\n- Saver 직렬화 포맷: `String[8] = [applied.pos.x, applied.pos.y, applied.rotation, applied.pivot.x, applied.pivot.y, draft.position, draft.rotation, draft.pivot]`\n- `rememberSaveable`로 구성 변경 및 시스템에 의한 프로세스 재생성(인스턴스 상태 복원 경로) 시 복원됩니다.\n\n## MVI 아키텍처\n\n본 프로젝트의 상태 관리는 MVI(Model–View–Intent) 패턴을 따릅니다. 단방향 데이터 흐름으로 입력 → 인텐트 → 리듀서 → 상태 → 렌더링 순으로 진행됩니다.\n\n- Intent: `TransformationIntent` (입력 변화·Apply 등 사용자 의도)\n- Reducer: `TransformIntent.intent(...)` (인텐트를 해석하여 `TransformControllerState`에 반영)\n- State: `TransformControllerState(applied + draft)` (UI가 구독하여 렌더)\n- View: `TransformControllerScreen` + `TransformControls` (입력 이벤트를 Intent로 디스패치)\n\n흐름도\n\n```mermaid\nflowchart LR\n  UI[\"View\u003cbr/\u003eTransformControls\"] -- onChange/onClick --\u003e I[\"TransformationIntent\"]\n  I --\u003e R[\"Reducer\u003cbr/\u003eTransformIntent.intent\"]\n  R --\u003e S[\"State\u003cbr/\u003eTransformControllerState\"]\n  S --\u003e V[\"View\u003cbr/\u003eCanvas/Vertex Display\"]\n```\n\n핵심 규칙\n- UI는 상태를 직접 수정하지 않고, 항상 Intent를 통해서만 상태가 변경됩니다.\n- Apply 버튼을 누를 때만 draft가 applied로 커밋되어 도형/좌표가 갱신됩니다.\n\n## 개념 정리 (Position vs Pivot, 좌표계)\n\n- Position (`TransformState.position`)\n  - 의미: 피벗의 전역(Global) 좌표\n  - 표시: Canvas의 빨간 점으로 렌더링됨\n  - 용도: 회전이 끝난 도형을 Global 공간으로 평행 이동하는 기준\n\n- Pivot (`TransformState.pivot`)\n  - 의미: 도형(Local 좌표계)에서의 회전 중심점\n  - 용도: 각 정점을 `pivot` 기준으로 회전\n\n- 좌표계\n  - Local: 사각형 로컬 좌표. 좌하단 원점, X→오른쪽+, Y→위쪽+ (초기 정점 정의 기준)\n  - Global: X→오른쪽+, Y→위쪽+, 가이드 범위는 −250 ~ 250 (뷰포트 내에서 원점은 중앙)\n  - Canvas: Android 픽셀 좌표. 좌상단 원점, X→오른쪽+, Y→아래쪽+\n  - 변환: `TransformCalculator`는 Local→Global 변환(회전/이동), `CoordinateSystem`은 Global→Canvas 변환 담당\n\n## 입력 규칙 및 폴백 동작\n\n- Position, Pivot 입력\n  - 형식: `\"x, y\"` (쉼표로 구분, 공백 허용, 소수 가능)\n  - 예: `\"0, 0\"`, `\"12.5, -30\"`\n  - 파싱 실패 시: `Offset.Zero`로 폴백됩니다. (경고/에러 UI 없음)\n\n- Rotation 입력\n  - 형식: 숫자(도 단위). 예: `\"0\"`, `\"45\"`, `\"-30\"`, `\"12.5\"`\n  - 파싱 실패 시: `0f`로 폴백됩니다.\n\n- 적용 버튼\n  - `Apply` 클릭 시 현재 입력값이 `TransformState`로 반영됩니다.\n\n## 동작 원리\n\n### 변환 흐름\n\n```mermaid\ngraph TD\n    U[\"사용자 입력\u003cbr/\u003e- Position x,y\u003cbr/\u003e- Rotation deg\u003cbr/\u003e- Pivot x,y\"] --\u003e I[\"TransformationIntent\u003cbr/\u003e(Input*)\"]\n    I --\u003e D[\"Draft 업데이트\u003cbr/\u003eTransformControllerState.draft\"]\n\n    A[\"Apply 버튼 클릭\"] --\u003e IA[\"TransformationIntent.Apply\"]\n    IA --\u003e R[\"Reducer\u003cbr/\u003eTransformIntent.intent\"]\n    R --\u003e S[\"Applied 갱신\u003cbr/\u003eTransformControllerState.applied\"]\n\n    S --\u003e C[\"TransformCalculator\"]\n    C --\u003e V1[\"초기 꼭짓점 생성\"]\n    V1 --\u003e V2[\"Pivot 기준 회전\"]\n    V2 --\u003e V3[\"Position 만큼 이동\"]\n    V3 --\u003e M[\"CoordinateSystem\u003cbr/\u003eGlobal → Canvas 변환\"]\n    M --\u003e O[\"Canvas 렌더링\u003cbr/\u003eGrid / Axis / Rectangle / Pivot\"]\n```\n\n## 기술 스택\n\n- **Language**: Kotlin\n- **UI Framework**: Jetpack Compose\n- **Build System**: Gradle (Kotlin DSL)\n- **Min SDK**: 24 (Android 7.0)\n- **Target SDK**: 36\n\n### 주요 라이브러리\n\n```kotlin\ndependencies {\n    // Jetpack Compose\n    implementation(\"androidx.compose.ui\")\n    implementation(\"androidx.compose.material3\")\n    implementation(\"androidx.activity.compose\")\n\n    // Lifecycle\n    implementation(\"androidx.lifecycle.runtime.ktx\")\n}\n```\n\n## UI 구성\n\n```mermaid\ngraph TD\n    U1[\"Transform Controller 화면\"] --\u003e U2[\"Drawing Area (Canvas)\u003cbr/\u003e- Grid\u003cbr/\u003e- Axis\u003cbr/\u003e- Rectangle\u003cbr/\u003e- Pivot\"]\n    U1 --\u003e U3[\"Vertex Coordinates\u003cbr/\u003eLT / RT / RB / LB 좌표 표시\"]\n    U1 --\u003e U4[\"Controls\u003cbr/\u003ePosition x,y / Rotation deg / Pivot x,y / Apply\"]\n```\n\n## 테스트 시나리오\n\n- 기본\n  - Position: `0, 0`, Pivot: `0, 0`, Rotation: `0`\n  - 기대: 사각형이 원점 기준으로 축과 정렬됨. 빨간 점은 중앙.\n\n- 회전만\n  - Position: `0, 0`, Pivot: `0, 0`, Rotation: `45`\n  - 기대: 원점 기준 45° 회전. 정점 좌표는 대칭적이고 좌표 표시가 ±값으로 변함.\n\n- Pivot 이동 + 회전\n  - Position: `0, 0`, Pivot: `10, 10`, Rotation: `30`\n  - 기대: 도형이 Local(10,10)을 중심으로 회전한 후 Global 원점에 위치. 빨간 점은 중심(0,0).\n\n- Global 이동\n  - Position: `100, 50`, Pivot: `0, 0`, Rotation: `0`\n  - 기대: 도형이 우측/상단으로 평행 이동. 빨간 점은 (100,50).\n\n- 혼합\n  - Position: `100, 50`, Pivot: `10, 10`, Rotation: `30`\n  - 기대: Local(10,10) 기준 회전 → Global(100,50)으로 이동. 좌표 표시와 Canvas 렌더링 일치.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteddko%2Ftedd_transformation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fteddko%2Ftedd_transformation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteddko%2Ftedd_transformation/lists"}