diff --git a/src/pages/agent-office/canvas/FurnitureRenderer.js b/src/pages/agent-office/canvas/FurnitureRenderer.js new file mode 100644 index 0000000..aed99d2 --- /dev/null +++ b/src/pages/agent-office/canvas/FurnitureRenderer.js @@ -0,0 +1,209 @@ +// src/pages/agent-office/canvas/FurnitureRenderer.js + +/** + * 가구 프로시저럴 렌더러 — 테마 팔레트 기반 + * 각 가구 타입별 draw 함수, Y-sort를 위한 zY 반환 + */ +export class FurnitureRenderer { + constructor(furnitureList, tileSize) { + this.furnitureList = furnitureList; + this.tileSize = tileSize; + } + + /** + * 모든 가구를 Y-sort 순서로 반환 (에이전트와 함께 정렬하기 위함) + * @returns {Array<{type, col, row, zY, draw: Function}>} + */ + getRenderables(theme, scale, offsetX, offsetY) { + const ts = this.tileSize * scale; + return this.furnitureList.map(f => ({ + ...f, + zY: f.row, + draw: (ctx) => this._drawFurniture(ctx, f, theme, ts, offsetX, offsetY) + })); + } + + _drawFurniture(ctx, f, theme, ts, ox, oy) { + const x = f.col * ts + ox; + const y = f.row * ts + oy; + + switch (f.type) { + case 'desk_monitor': this._drawDesk(ctx, f, theme, ts, x, y); break; + case 'meeting_table': this._drawMeetingTable(ctx, f, theme, ts, x, y); break; + case 'sofa': this._drawSofa(ctx, theme, ts, x, y); break; + case 'coffee_machine':this._drawCoffeeMachine(ctx, theme, ts, x, y); break; + case 'bookshelf': this._drawBookshelf(ctx, f, theme, ts, x, y); break; + case 'plant': this._drawPlant(ctx, theme, ts, x, y); break; + case 'water_cooler': this._drawWaterCooler(ctx, theme, ts, x, y); break; + } + } + + _drawDesk(ctx, f, theme, ts, x, y) { + // 책상 상판 + const dw = ts * 2; + const dh = ts * 0.6; + ctx.fillStyle = theme.furniture.desk; + ctx.fillRect(x, y + ts * 0.2, dw, dh); + // 책상 다리 + ctx.fillStyle = theme.wall.border; + ctx.fillRect(x + ts * 0.1, y + dh + ts * 0.2, ts * 0.15, ts * 0.3); + ctx.fillRect(x + dw - ts * 0.25, y + dh + ts * 0.2, ts * 0.15, ts * 0.3); + + // 모니터들 + const monCount = f.monitors || 1; + const monW = ts * 0.5; + const monH = ts * 0.4; + const totalW = monCount * monW + (monCount - 1) * ts * 0.1; + let monX = x + (dw - totalW) / 2; + + for (let i = 0; i < monCount; i++) { + // 모니터 프레임 + ctx.fillStyle = theme.furniture.monitor; + ctx.fillRect(monX, y - monH + ts * 0.2, monW, monH); + // 화면 + ctx.fillStyle = theme.furniture.monitorScreen; + ctx.fillRect(monX + ts * 0.05, y - monH + ts * 0.25, monW - ts * 0.1, monH - ts * 0.1); + // 모니터 받침대 + ctx.fillStyle = theme.furniture.monitor; + ctx.fillRect(monX + monW * 0.35, y + ts * 0.2 - ts * 0.05, monW * 0.3, ts * 0.08); + monX += monW + ts * 0.1; + } + + // 의자 (책상 아래) + ctx.fillStyle = theme.furniture.chair; + ctx.fillRect(x + dw * 0.35, y + ts, dw * 0.3, ts * 0.5); + ctx.fillRect(x + dw * 0.3, y + ts * 0.8, dw * 0.4, ts * 0.25); + + // 에이전트별 악센트 소품 + if (f.accent === 'instrument') { + // 음표 모양 + ctx.fillStyle = theme.ui.accent; + ctx.fillRect(x + dw + ts * 0.2, y + ts * 0.3, ts * 0.1, ts * 0.5); + ctx.beginPath(); + ctx.arc(x + dw + ts * 0.2, y + ts * 0.8, ts * 0.15, 0, Math.PI * 2); + ctx.fill(); + } else if (f.accent === 'papers') { + // 서류 더미 + ctx.fillStyle = '#ffffff'; + ctx.fillRect(x + dw + ts * 0.1, y + ts * 0.3, ts * 0.35, ts * 0.45); + ctx.fillStyle = theme.text.label; + for (let i = 0; i < 3; i++) { + ctx.fillRect(x + dw + ts * 0.15, y + ts * 0.38 + i * ts * 0.1, ts * 0.25, ts * 0.02); + } + } else if (f.accent === 'briefcase') { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(x + dw + ts * 0.1, y + ts * 0.5, ts * 0.4, ts * 0.3); + ctx.fillStyle = '#D4A06A'; + ctx.fillRect(x + dw + ts * 0.2, y + ts * 0.45, ts * 0.2, ts * 0.08); + } else if (f.accent === 'dice') { + ctx.fillStyle = '#ef4444'; + ctx.fillRect(x + dw + ts * 0.15, y + ts * 0.4, ts * 0.3, ts * 0.3); + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.arc(x + dw + ts * 0.3, y + ts * 0.55, ts * 0.05, 0, Math.PI * 2); + ctx.fill(); + } + } + + _drawMeetingTable(ctx, f, theme, ts, x, y) { + const w = (f.width || 4) * ts; + const h = (f.height || 2) * ts; + // 테이블 상판 + ctx.fillStyle = theme.furniture.table; + ctx.fillRect(x + ts * 0.1, y + ts * 0.1, w - ts * 0.2, h - ts * 0.2); + // 테이블 그림자 + ctx.fillStyle = 'rgba(0,0,0,0.15)'; + ctx.fillRect(x + ts * 0.15, y + h - ts * 0.1, w - ts * 0.25, ts * 0.1); + // 의자들 (상하 4개씩) + for (let i = 0; i < 4; i++) { + const cx = x + ts * 0.5 + i * (w - ts) / 3; + ctx.fillStyle = theme.furniture.chair; + ctx.fillRect(cx, y - ts * 0.3, ts * 0.4, ts * 0.35); + ctx.fillRect(cx, y + h - ts * 0.05, ts * 0.4, ts * 0.35); + } + } + + _drawSofa(ctx, theme, ts, x, y) { + ctx.fillStyle = theme.furniture.sofa; + ctx.fillRect(x, y, ts * 2, ts * 0.8); + // 등받이 + ctx.fillStyle = theme.furniture.sofa; + ctx.fillRect(x, y - ts * 0.3, ts * 2, ts * 0.35); + // 쿠션 구분선 + ctx.strokeStyle = theme.wall.border; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x + ts, y); + ctx.lineTo(x + ts, y + ts * 0.8); + ctx.stroke(); + } + + _drawCoffeeMachine(ctx, theme, ts, x, y) { + ctx.fillStyle = theme.furniture.coffee; + ctx.fillRect(x + ts * 0.15, y, ts * 0.7, ts * 0.8); + // 디스펜서 + ctx.fillStyle = theme.furniture.monitor; + ctx.fillRect(x + ts * 0.25, y + ts * 0.15, ts * 0.5, ts * 0.3); + // 커피 잔 + ctx.fillStyle = '#ffffff'; + ctx.fillRect(x + ts * 0.3, y + ts * 0.55, ts * 0.2, ts * 0.15); + // 스팀 + ctx.strokeStyle = 'rgba(255,255,255,0.3)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(x + ts * 0.4, y + ts * 0.5); + ctx.quadraticCurveTo(x + ts * 0.45, y + ts * 0.35, x + ts * 0.4, y + ts * 0.2); + ctx.stroke(); + } + + _drawBookshelf(ctx, f, theme, ts, x, y) { + const h = (f.height || 3) * ts; + ctx.fillStyle = theme.furniture.shelf; + ctx.fillRect(x, y, ts * 0.9, h); + // 선반 및 책 + const bookColors = ['#aa4444', '#4444aa', '#44aa44', '#aaaa44', '#aa44aa', '#44aaaa']; + const shelfCount = f.height || 3; + for (let i = 0; i < shelfCount; i++) { + const sy = y + i * ts + ts * 0.1; + // 선반 판 + ctx.fillStyle = theme.furniture.shelf; + ctx.fillRect(x, sy + ts * 0.7, ts * 0.9, ts * 0.05); + // 책들 + for (let b = 0; b < 4; b++) { + ctx.fillStyle = bookColors[(i * 4 + b) % bookColors.length]; + ctx.fillRect(x + ts * 0.05 + b * ts * 0.2, sy + ts * 0.1, ts * 0.15, ts * 0.6); + } + } + } + + _drawPlant(ctx, theme, ts, x, y) { + // 화분 + ctx.fillStyle = theme.decor.pot; + ctx.fillRect(x + ts * 0.25, y + ts * 0.6, ts * 0.5, ts * 0.35); + ctx.fillRect(x + ts * 0.2, y + ts * 0.55, ts * 0.6, ts * 0.1); + // 잎 + ctx.fillStyle = theme.decor.plant; + ctx.beginPath(); + ctx.ellipse(x + ts * 0.5, y + ts * 0.35, ts * 0.3, ts * 0.25, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + ts * 0.35, y + ts * 0.25, ts * 0.15, ts * 0.2, -0.3, 0, Math.PI * 2); + ctx.fill(); + ctx.beginPath(); + ctx.ellipse(x + ts * 0.65, y + ts * 0.25, ts * 0.15, ts * 0.2, 0.3, 0, Math.PI * 2); + ctx.fill(); + } + + _drawWaterCooler(ctx, theme, ts, x, y) { + // 본체 + ctx.fillStyle = theme.furniture.shelf; + ctx.fillRect(x + ts * 0.2, y + ts * 0.3, ts * 0.6, ts * 0.6); + // 물통 + ctx.fillStyle = 'rgba(100,180,255,0.5)'; + ctx.fillRect(x + ts * 0.3, y, ts * 0.4, ts * 0.35); + ctx.fillStyle = 'rgba(100,180,255,0.3)'; + ctx.beginPath(); + ctx.arc(x + ts * 0.5, y, ts * 0.2, 0, Math.PI * 2); + ctx.fill(); + } +}