Merge remote-tracking branch 'origin/sub_desktop_1' into sub_laptop_1
This commit is contained in:
@@ -120,9 +120,13 @@ async def _title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
|
||||
사람 배정(과업책임자·분야별책임자·설계자)은 `projects`의 FK를 따라간다. 설계자는
|
||||
배정이 없으면 프로젝트 소유자로 떨어진다 — 혼자 쓰는 계정에서도 칸이 차게.
|
||||
로고·서명은 프로젝트가 회사 공유 자산(`company_assets`, 013)에서 고른 것만 싣는다 —
|
||||
안 골랐거나 자산이 지워졌으면 그림째 빠진다. 011 의 `companies.logo_path` ·
|
||||
`users.signature_path` 는 더 읽지 않는다.
|
||||
로고·서명은 회사 공유 자산(`company_assets`, 013)에서 오되 **원천이 다르다**
|
||||
(2026-09-02 사용자 확정, B01 인계). 서명은 프로젝트가 아니라 **사람 계정**에 붙으므로
|
||||
설계자(`designer_user_id`, 없으면 소유자) 계정의 `SIGNATURE` 자산을 따라간다. 로고는
|
||||
프로젝트가 고른 것이 우선이고, 안 골랐으면 **회사 등록 단계에서 받은 회사 로고**
|
||||
(`companies.logo_asset_id`, 014)로 떨어진다. 둘 다 없거나 자산이 지워졌으면 그림째
|
||||
빠진다. `projects.signature_asset_id` 는 컬럼만 남기고 더 읽지 않는다(B01 이 항상
|
||||
`null` 로 저장). 011 의 `companies.logo_path` · `users.signature_path` 도 안 읽는다.
|
||||
|
||||
아직 못 채우는 자리와 이유:
|
||||
- 사업량·연도기번은 사람이 넣는 값이다(B01 프로젝트 수정 화면). 비어 있으면 빈칸.
|
||||
@@ -145,9 +149,18 @@ async def _title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
LEFT JOIN users pm ON pm.id = p.pm_user_id
|
||||
LEFT JOIN users lead ON lead.id = p.field_lead_user_id
|
||||
LEFT JOIN company_assets logo
|
||||
ON logo.id = p.logo_asset_id AND logo.deleted_at IS NULL
|
||||
ON logo.id = COALESCE(p.logo_asset_id, c.logo_asset_id)
|
||||
AND logo.deleted_at IS NULL
|
||||
LEFT JOIN company_assets sig
|
||||
ON sig.id = p.signature_asset_id AND sig.deleted_at IS NULL
|
||||
ON sig.id = (
|
||||
SELECT s.id
|
||||
FROM company_assets s
|
||||
WHERE s.user_id = COALESCE(p.designer_user_id, p.user_id)
|
||||
AND s.kind = 'SIGNATURE'
|
||||
AND s.deleted_at IS NULL
|
||||
ORDER BY s.id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE p.id = %s AND p.deleted_at IS NULL
|
||||
""",
|
||||
(str(project_id),),
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
export interface FrameTemplateEditor {
|
||||
/** 도면 목록 아래에 놓는 「도각 편집」 버튼. */
|
||||
button: HTMLButtonElement;
|
||||
/** 편집 중임을 알리는 CAD 화면 상단 띠 (평소엔 숨김). */
|
||||
/** 편집 중임을 알리는 띠 — 도면 목록 하단 액션 칸의 1행 (평소엔 숨김). */
|
||||
banner: HTMLElement;
|
||||
/** 편집 중인가 — 도면 변경 알림(확정 해제)을 이 동안 막는 데 쓴다. */
|
||||
isEditing: () => boolean;
|
||||
@@ -66,7 +66,10 @@ export function createFrameTemplateEditor(options: Options): FrameTemplateEditor
|
||||
variant: "ghost",
|
||||
onClick: () => leave(),
|
||||
});
|
||||
banner.append(finishButton, resetButton, cancelButton);
|
||||
const bannerButtons = document.createElement("div");
|
||||
bannerButtons.className = "b07-frame-edit__buttons";
|
||||
bannerButtons.append(finishButton, resetButton, cancelButton);
|
||||
banner.append(bannerButtons);
|
||||
|
||||
const button = createButton({
|
||||
label: "도각 편집",
|
||||
|
||||
@@ -601,7 +601,6 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
},
|
||||
onSaved: () => drawingCache.clear(),
|
||||
});
|
||||
cadHost.prepend(frameEditor.banner);
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent<unknown>) => {
|
||||
if (event.origin !== window.location.origin || event.source !== frame.contentWindow) return;
|
||||
@@ -668,7 +667,12 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
// 하단 고정은 공용 ui-sidebar-actions로 통일 — 사이드 본문이 [스크롤 영역][액션 줄]로
|
||||
// 쪼개지고 액션 줄은 스크롤 밖에 남는다(2026-08-18 사용자 지시, B04~B07 공통).
|
||||
confirmActions.className = "b07-drawing-actions ui-sidebar-actions";
|
||||
confirmActions.append(frameEditor.button, confirmButton);
|
||||
// 액션 칸은 두 줄이다 — 1행 도각 편집 띠(평소 숨김), 2행 [도각 편집]·[현재 도면 확정]
|
||||
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김).
|
||||
const confirmButtonRow = document.createElement("div");
|
||||
confirmButtonRow.className = "b07-drawing-actions__row";
|
||||
confirmButtonRow.append(frameEditor.button, confirmButton);
|
||||
confirmActions.append(frameEditor.banner, confirmButtonRow);
|
||||
|
||||
drawingPanel.append(infoPanelHost, confirmActions);
|
||||
|
||||
|
||||
@@ -126,11 +126,25 @@
|
||||
|
||||
/* 하단 고정·배경·상단 구분선은 공용 ui-sidebar-actions가 맡는다(2026-08-18 통합).
|
||||
여기서는 B07 고유 여백만 남긴다. */
|
||||
/* 액션 칸은 세로 두 줄 — 1행 도각 편집 띠, 2행 [도각 편집]·[현재 도면 확정].
|
||||
공용 .ui-sidebar-actions는 가로 한 줄이라 방향과 늘어남을 여기서 되돌린다. */
|
||||
.b07-drawing-actions {
|
||||
flex-direction: column;
|
||||
padding-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
.b07-drawing-actions > button {
|
||||
.b07-drawing-actions > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.b07-drawing-actions__row {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-drawing-actions__row > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -271,29 +285,36 @@
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 도각 편집 모드 띠 — CAD 위에 겹쳐 편집 중임을 알리고 [완료]·[취소]를 준다. */
|
||||
/* 도각 편집 모드 띠 — 도면 목록 하단 액션 칸의 1행. CAD 위에 떠 있던 배치는 리본과
|
||||
겹쳐 문구가 접히고 버튼이 찌그러졌다(2026-09-02 사용자 지시로 사이드바로 옮김). */
|
||||
.b07-frame-edit {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: 6px 10px;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: 0 6px 18px rgb(0 0 0 / 25%);
|
||||
}
|
||||
|
||||
.b07-frame-edit[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-frame-edit__label {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b07-frame-edit > button {
|
||||
padding: 4px 12px;
|
||||
.b07-frame-edit__buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b07-frame-edit__buttons > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** 뷰 탭 — 탐색·재생성 명령 (조사표 7절 중 구현분) */
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { CadCommand } from './command.types';
|
||||
import { bumpSceneVersion } from '../helpers/scene-version';
|
||||
@@ -8,8 +9,23 @@ import { selectToolStateMachine } from '../tools/select-tool';
|
||||
|
||||
const zoomBy = (factor: number): string => {
|
||||
const controller = getScreenCanvasDrawController();
|
||||
controller.setScreenScale(Math.max(0.01, controller.getScreenScale() * factor));
|
||||
return `줌 ${Math.round(controller.getScreenScale() * 100)}%`;
|
||||
const oldScale = controller.getScreenScale();
|
||||
const newScale = Math.max(0.01, oldScale * factor);
|
||||
// 배율만 바꾸면 화면이 월드 원점 쪽으로 늘어나 도면이 화면 밖으로 밀려난다.
|
||||
// 휠 줌이 커서 밑 좌표를 붙잡아 두듯, 버튼 줌은 **화면 중심**의 월드 좌표를
|
||||
// 붙잡아 둔다 (screen = (world - offset) * scale).
|
||||
const canvasSize = controller.getCanvasSize();
|
||||
const offset = controller.getScreenOffset();
|
||||
const worldCenterX = offset.x + canvasSize.x / 2 / oldScale;
|
||||
const worldCenterY = offset.y + canvasSize.y / 2 / oldScale;
|
||||
controller.setScreenScale(newScale);
|
||||
controller.setScreenOffset(
|
||||
new Point(
|
||||
worldCenterX - canvasSize.x / 2 / newScale,
|
||||
worldCenterY - canvasSize.y / 2 / newScale
|
||||
)
|
||||
);
|
||||
return `줌 ${Math.round(newScale * 100)}%`;
|
||||
};
|
||||
|
||||
export const VIEW_COMMANDS: CadCommand[] = [
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ViewControls } from './ViewControls';
|
||||
export const Toolbar: FC = () => {
|
||||
useCadRefresh();
|
||||
const [panelCollapsed, setPanelCollapsed] = useState(false);
|
||||
const [commandLineVisible, setCommandLineVisible] = useState(true);
|
||||
const [commandLineVisible, setCommandLineVisible] = useState(false);
|
||||
|
||||
const activeTool = (getActiveToolActor()?.getSnapshot()?.context?.type ?? null) as Tool | null;
|
||||
|
||||
|
||||
@@ -596,6 +596,11 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
angle: number
|
||||
): void {
|
||||
if (this.batching) this.flushBatch();
|
||||
// 못 읽은 그림은 건너뛴다. 'broken' 상태의 그림을 그리려 하면 캔버스가 예외를
|
||||
// 던지고, 그 예외가 렌더 루프를 끊어 **이후 모든 도면이 백지**로 남았다
|
||||
// (2026-09-02 실측: 도각 자리표시자가 404 로 깨진 채 줌 한 번에 화면이 멈췄다).
|
||||
// 자리 하나가 비는 것과 도면 전체가 안 나오는 것은 무게가 다르다.
|
||||
if (imageElement.complete === false || imageElement.naturalWidth === 0) return;
|
||||
// 크기는 배율만 곱한다. 예전에는 (width, height)를 좌표처럼 변환해 화면 오프셋과
|
||||
// y 뒤집기가 섞여 들어갔고, 그림이 제 자리를 벗어나 비율까지 무너졌다(2026-09-02
|
||||
// 도각 로고·서명에서 드러남). 자리는 SVG 컨트롤러와 같이 **세계 중심**으로 잡는다.
|
||||
|
||||
@@ -26,6 +26,13 @@ export class ImageEntity implements Entity {
|
||||
private imageElement: HTMLImageElement;
|
||||
private polygon: Polygon;
|
||||
private angle: number;
|
||||
/**
|
||||
* JSON에서 받은 그림 주소 원본. `imageElement.currentSrc`는 브라우저가 절대 URL로
|
||||
* 바꿔 놓아, 도각의 자리표시자(`{{회사로고}}`)가 저장 한 번에
|
||||
* `http://…/b07-cad/%7B%7B회사로고%7D%7D`로 굳는다(2026-09-02 실측 — 회사 도각이
|
||||
* 그렇게 손상됐다). 원본을 들고 있다가 그대로 돌려준다.
|
||||
*/
|
||||
private sourceData: string | null = null;
|
||||
|
||||
constructor(
|
||||
layerId: string,
|
||||
@@ -115,7 +122,9 @@ export class ImageEntity implements Entity {
|
||||
public clone(): ImageEntity {
|
||||
const clonedImage = document.createElement('img');
|
||||
clonedImage.src = this.imageElement.src;
|
||||
return new ImageEntity(getActiveLayerId(), clonedImage, this.polygon.clone());
|
||||
const cloned = new ImageEntity(getActiveLayerId(), clonedImage, this.polygon.clone());
|
||||
cloned.sourceData = this.sourceData;
|
||||
return cloned;
|
||||
}
|
||||
|
||||
// TODO add destroy method to cleanup this.imageElement.src
|
||||
@@ -230,7 +239,7 @@ export class ImageEntity implements Entity {
|
||||
x: vertex.x,
|
||||
y: vertex.y,
|
||||
})),
|
||||
imageData: this.imageElement.currentSrc,
|
||||
imageData: this.sourceData ?? this.imageElement.currentSrc,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -243,7 +252,12 @@ export class ImageEntity implements Entity {
|
||||
jsonEntity.shapeData.points.map((point) => new Point(point.x, point.y))
|
||||
);
|
||||
const image = new Image();
|
||||
image.src = jsonEntity.shapeData.imageData;
|
||||
// 자리표시자는 주소가 아니다 — 그대로 넣으면 404 요청이 나가고 그림이 'broken'
|
||||
// 상태가 된다. 그 상태의 그림을 그리려 하면 캔버스가 예외를 던져 렌더 루프가
|
||||
// 끊기고 **이후 모든 도면이 백지**로 남았다(2026-09-02 실측). 자리만 남긴다.
|
||||
if (!jsonEntity.shapeData.imageData.includes('{{')) {
|
||||
image.src = jsonEntity.shapeData.imageData;
|
||||
}
|
||||
const rectangleEntity = new ImageEntity(
|
||||
jsonEntity.layerId || getActiveLayerId(),
|
||||
image,
|
||||
@@ -253,6 +267,7 @@ export class ImageEntity implements Entity {
|
||||
rectangleEntity.lineColor = jsonEntity.lineColor;
|
||||
rectangleEntity.lineWidth = jsonEntity.lineWidth;
|
||||
rectangleEntity.lineDash = jsonEntity.lineDash;
|
||||
rectangleEntity.sourceData = jsonEntity.shapeData.imageData;
|
||||
return rectangleEntity;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user