// ๐Ÿงฉ ๋Œ€์‹œ๋ณด๋“œ ํƒ€์ผ ๋ฐฐ์น˜(์œ„์น˜/ํฌ๊ธฐ)์˜ ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ ๋‹ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ. // ์บ”๋ฒ„์Šค๋Š” 6์—ดร—4ํ–‰ ๊ณ ์ •, ํƒ€์ผ ํฌ๊ธฐ๋Š” 1x1~4x4. ๊ณ„์ •๋ณ„๋กœ ์„œ๋ฒ„(๊ณ„์ •=userId)์— ์ €์žฅํ•œ๋‹ค. import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../config.dart'; import '../models/dashboard_tile_layout.dart'; class DashboardLayoutController extends ChangeNotifier { final String userId; DashboardLayoutController(this.userId); Map _positions = {}; bool _isLoading = true; bool _isSaving = false; bool _isEditing = false; Map get positions => _positions; bool get isLoading => _isLoading; bool get isSaving => _isSaving; bool get isEditing => _isEditing; void enterEditMode() { _isEditing = true; notifyListeners(); } /// ํŽธ์ง‘ ์ค‘์ด๋˜ ๋ณ€๊ฒฝ์„ ์„œ๋ฒ„์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ ์ฑ„ ํŽธ์ง‘ ๋ชจ๋“œ๋งŒ ๋ˆ๋‹ค. /// (์ €์žฅ์€ [save]๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์•ผ ํ•จ โ€” ์™„๋ฃŒ ๋ฒ„ํŠผ์ด save+exit๋ฅผ ๊ฐ™์ด ํ˜ธ์ถœํ•œ๋‹ค.) void exitEditMode() { _isEditing = false; notifyListeners(); } /// ์„œ๋ฒ„์— ์ €์žฅ๋œ ๋ฐฐ์น˜๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ , ํ˜„์žฌ ํ™”๋ฉด์— ๋ณด์—ฌ์•ผ ํ•  ํƒ€์ผ id ๋ชฉ๋ก([visibleIds]) ์ค‘ /// ์ €์žฅ๋œ ๊ฐ’์ด ์—†๋Š” ์ƒˆ ํƒ€์ผ์€ ๋นˆ ์นธ์„ ์ฐพ์•„ 1x1๋กœ ์ž๋™ ๋ฐฐ์น˜ํ•œ๋‹ค. Future load(List visibleIds) async { _isLoading = true; notifyListeners(); Map loaded = {}; try { final response = await http.get( Uri.parse('$baseUrl/api/dashboard/layout?userId=$userId'), ); if (response.statusCode == 200) { final data = jsonDecode(utf8.decode(response.bodyBytes)); final rawLayout = data['layout']; if (rawLayout is List) { for (final item in rawLayout) { final id = item['id']?.toString(); if (id == null) continue; final rect = TileRect.fromJson(item); if (rect.isWithinCanvas) loaded[id] = rect; } } } } catch (_) { // ์ƒˆ๋กœ๊ณ ์นจ ์‹คํŒจ๋Š” ์กฐ์šฉํžˆ ๋ฌด์‹œํ•˜๊ณ  ๊ธฐ๋ณธ ๋ฐฐ์น˜๋กœ ๋Œ€์ฒดํ•œ๋‹ค. } _positions = _fillDefaults(loaded, visibleIds); _isLoading = false; notifyListeners(); } Map _fillDefaults( Map existing, List ids, ) { final Map result = {}; for (final id in ids) { final saved = existing[id]; if (saved != null && !_collidesWithAny(result, saved, null)) { result[id] = saved; } } for (final id in ids) { if (result.containsKey(id)) continue; final spot = _findFirstFit(result, 1, 1); if (spot != null) { result[id] = TileRect(x: spot.$1, y: spot.$2, w: 1, h: 1); } // ์บ”๋ฒ„์Šค(6x4=24์นธ)๊ฐ€ ๊ฝ‰ ์ฐผ์œผ๋ฉด ์ž๋ฆฌ๋ฅผ ๋ชป ์ฐพ์„ ์ˆ˜๋„ ์žˆ์Œ โ€” ๊ทธ ํƒ€์ผ์€ ํ™”๋ฉด์— ์•ˆ ๋ณด์ด๊ฒŒ ๋จ. } return result; } bool _collidesWithAny( Map placed, TileRect candidate, String? ignoreId, ) { for (final entry in placed.entries) { if (entry.key == ignoreId) continue; if (entry.value.overlaps(candidate)) return true; } return false; } (int, int)? _findFirstFit(Map placed, int w, int h) { for (int y = 0; y <= kGridRows - h; y++) { for (int x = 0; x <= kGridCols - w; x++) { final candidate = TileRect(x: x, y: y, w: w, h: h); if (!_collidesWithAny(placed, candidate, null)) return (x, y); } } return null; } bool _fits(String movingId, TileRect candidate) { if (!candidate.isWithinCanvas) return false; return !_collidesWithAny(_positions, candidate, movingId); } /// ํƒ€์ผ์„ (newX, newY)๋กœ ์˜ฎ๊ธด๋‹ค. ๋‹ค๋ฅธ ํƒ€์ผ๊ณผ ๊ฒน์น˜๊ฑฐ๋‚˜ ์บ”๋ฒ„์Šค๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ๋ฌด์‹œํ•˜๊ณ  false ๋ฐ˜ํ™˜. bool tryMove(String id, int newX, int newY) { final cur = _positions[id]; if (cur == null) return false; final candidate = cur.copyWith(x: newX, y: newY); if (!_fits(id, candidate)) return false; _positions = {..._positions, id: candidate}; notifyListeners(); return true; } /// ํƒ€์ผ ํฌ๊ธฐ๋ฅผ (newW, newH)๋กœ ๋ฐ”๊พผ๋‹ค. ๋‹ค๋ฅธ ํƒ€์ผ๊ณผ ๊ฒน์น˜๊ฑฐ๋‚˜ 4x4/์บ”๋ฒ„์Šค๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ๋ฌด์‹œ. bool tryResize(String id, int newW, int newH) { final cur = _positions[id]; if (cur == null) return false; final candidate = cur.copyWith(w: newW, h: newH); if (!_fits(id, candidate)) return false; _positions = {..._positions, id: candidate}; notifyListeners(); return true; } Future save() async { _isSaving = true; notifyListeners(); try { final layout = _positions.entries .map((e) => e.value.toJson(e.key)) .toList(); await http.post( Uri.parse('$baseUrl/api/dashboard/layout'), headers: {"Content-Type": "application/json"}, body: jsonEncode({"userId": userId, "layout": layout}), ); } catch (_) { // ์ €์žฅ ์‹คํŒจํ•ด๋„ ๋กœ์ปฌ ๋ฐฐ์น˜๋Š” ์œ ์ง€ โ€” ๋‹ค์Œ์— ๋‹ค์‹œ ์‹œ๋„ ๊ฐ€๋Šฅ. } finally { _isSaving = false; _isEditing = false; notifyListeners(); } } }