가끔 보자, 하늘.

Flutter 로 앱 개발 및 릴리즈 - 05. Project 페이지 본문

개발 이야기/개발 및 서비스

Flutter 로 앱 개발 및 릴리즈 - 05. Project 페이지

가온아 2025. 5. 13. 09:00

오늘은 프로젝트 탭 페이지를 만들어 보겠습니다.

트리구조 형태에 드래그 앤 드랍으로 일정은 손쉽게 조정할 수 있게 할 컴포넌트가 필요한데 공개된 컴포넌트는 적당한게 없네요. 기본으로 제공되는 TreeNodeWidget을 활용해 개발할 것을 Gemini가 추천을 해주네요. 

이 프로젝트는 개발자를 위한 ToDo 앱을 개발하는 것이며, 일정을 프로젝트 별로 관리하도록 합니다. 상세한 데이터 구조는 뒤에서 다시 논의하고 오늘은 이름만 가진 임시 데이터 구조를 만들어 보겠습니다. lib/project_item.dart 파일을 생성하고 아래 코드를 추가합니다.

Gemin에게 요청:  트리 형태로 출력하고 하단에는 +, - 등의 조작 버튼을 만들어줘 트리 영역은 스크롤 가능하고 하단의 조작 버튼은 고정되게 해줘. 트리 내 모두 객체는 드래그 & 드랍으로 위치를 조정할 수 있어.
import 'package:flutter/material.dart';

class ProjectItem {
  String id; 	// 고유 id
  String name; 	// 일정 이름
  List<ProjectItem> children; // 자식 일정

  ProjectItem({required this.id, required this.name, this.children = const []});
}

lib/tree_node_widget.dart 파일을 생성하고 아래 코드를 추가합니다. 

import 'package:flutter/material.dart';
import 'project_item.dart'; // ProjectItem 모델 임포트

class TreeNodeWidget extends StatelessWidget {
  final ProjectItem item;
  final Function(String sourceId, String targetId) onDragComplete;

  const TreeNodeWidget({
    super.key,
    required this.item,
    required this.onDragComplete,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Draggable 위젯: 이 위젯을 드래그할 수 있도록 합니다.
        Draggable<String>(
          data: item.id, // 드래그될 때 전달할 데이터
          feedback: Material( // 드래그 중에 표시될 위젯
            elevation: 4.0,
            child: Container(
              padding: const EdgeInsets.all(8.0),
              child: Text(item.name),
            ),
          ),
          child: DragTarget<String>( // DragTarget 위젯: 다른 Draggable 위젯을 받을 수 있도록 합니다.
            onAccept: (data) {
              // 드래그된 데이터(sourceId)가 이 타겟(targetId)에 드롭되었을 때 호출됩니다.
              onDragComplete(data, item.id);
            },
            builder: (context, candidateData, rejectedData) {
              // 드래그 중일 때 타겟의 모양을 변경할 수 있습니다.
              return Container(
                decoration: BoxDecoration(
                  border: candidateData.isNotEmpty
                      ? Border.all(color: Colors.blueAccent, width: 2.0)
                      : null,
                ),
                padding: const EdgeInsets.all(8.0),
                margin: const EdgeInsets.symmetric(vertical: 4.0),
                child: Row(
                  children: [
                    const Icon(Icons.folder), // 트리 항목 아이콘 (예시)
                    const SizedBox(width: 8.0),
                    Text(item.name),
                  ],
                ),
              );
            },
          ),
        ),
        // 자식 노드를 재귀적으로 표시
        Padding(
          padding: const EdgeInsets.only(left: 16.0),
          child: Column(
            children: item.children.map((child) {
              return TreeNodeWidget(
                item: child,
                onDragComplete: onDragComplete,
              );
            }).toList(),
          ),
        ),
      ],
    );
  }
}

이제 lib/project_page.dart를 아래와 같이 생성합니다. 화면 하단에는 Node 추가 삭제를 위한 버튼을 추가합니다. 

import 'package:flutter/material.dart';
import 'project_item.dart'; // ProjectItem 모델 임포트
import 'tree_node_widget.dart'; // TreeNodeWidget 임포트

class ProjectPage extends StatefulWidget {
  const ProjectPage({super.key});

  @override
  State<ProjectPage> createState() => _ProjectPageState();
}

class _ProjectPageState extends State<ProjectPage> {
  // 예시 데이터입니다. 이번에는 drag & drop 시 실제 변경이 일어나지는 않습니다. 
  List<ProjectItem> _projectItems = [
    ProjectItem(id: '1', name: 'Project 1', children: [
      ProjectItem(id: '1.1', name: 'Subproject 1.1'),
      ProjectItem(id: '1.2', name: 'Subproject 1.2', children: [
        ProjectItem(id: '1.2.1', name: 'Task 1.2.1'),
      ]),
    ]),
    ProjectItem(id: '2', name: 'Project 2'),
  ];

  // 드래그 앤 드롭 완료 시 호출될 함수
  void _handleDragComplete(String sourceId, String targetId) {
    setState(() {
      // 여기에서 _projectItems 리스트를 업데이트하는 로직을 구현합니다.
      // sourceId를 가진 항목을 찾아서 targetId를 가진 항목의 자식으로 이동시키는 로직이 필요합니다.
      // 트리 구조에서의 드래그 앤 드롭 로직은 복잡하며, 부모-자식 관계 업데이트, 순서 변경 등을 고려해야 합니다.
      print('Dragged $sourceId to $targetId');
      // 실제 데이터 업데이트 로직을 추가해야 합니다.
      // 예시: 간단히 로그만 출력합니다.
    });
  }

  // 하단 버튼 클릭 시 실행될 함수들 (예시)
  void _addItem() {
    // 항목 추가 로직 구현
    print('Add button clicked');
  }

  void _removeItem() {
    // 항목 삭제 로직 구현
    print('Remove button clicked');
  }

  // 필요한 다른 조작 버튼 함수들을 여기에 추가합니다.
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Projects'),
      ),
      body: Column(
        children: [
          Expanded( // 트리 영역이 남은 공간을 모두 차지하고 스크롤 가능하도록 합니다.
            child: SingleChildScrollView( // 트리 영역 스크롤 가능
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: _projectItems.map((item) {
                    return TreeNodeWidget(
                      item: item,
                      onDragComplete: _handleDragComplete,
                    );
                  }).toList(),
                ),
              ),
            ),
          ),
          // 하단 고정 조작 버튼 영역
          Container(
            padding: const EdgeInsets.all(8.0),
            color: Colors.grey[200], // 버튼 영역 배경색 (예시)
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: _addItem, // 항목 추가 함수 연결
                  child: const Icon(Icons.add),
                ),
                ElevatedButton(
                  onPressed: _removeItem, // 항목 삭제 함수 연결
                  child: const Icon(Icons.remove),
                ),
                // 필요한 다른 조작 버튼을 여기에 추가합니다.
                // ElevatedButton(
                //   onPressed: () {
                //     // 다른 기능
                //     print('Another button clicked');
                //   },
                //   child: const Icon(Icons.edit),
                // ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

대부분의 주요 코드는 아직 작동하지 않고 이벤트 정보를 출력만 합니다. 이제 navi_bar.dart코드를 아래와 같이 수정합니다.

import 'package:flutter/material.dart';
import 'calendar_page.dart';
import 'project_page.dart';
.
.
  final List<Widget> _pages = <Widget>[
    const CalendarPage(),
    const ProjectPage(),
.
.
.

실행하면 아래와 같은 화면을 확인 할 수 있습니다.

기본 Widget이라 보기는 좀 썰렁 하네요. 첫 술에 배부를 수는 없으니까요 ^^

다음에는 Setting Page를 구성해 기본 틀을 모두 완료해 보겠습니다. :)

반응형