ABC 부트캠프 데이터 탐험가 4기

[36 일차] ABC 부트캠프 : 최종 프로젝트(6)

marriina 2024. 8. 25. 22:00

Flutter 코드는 자폐 조기진단을 위한 앱의 주요 흐름을 구현한 것입니다. 앱은 사용자가 개인정보를 입력하고, 설문조사를 완료한 후, 이미지나 영상을 업로드하여 자폐 스펙트럼 장애를 조기진단하는 기능을 제공합니다. 이 앱은 여러 화면을 통해 사용자의 정보를 수집하고 진단 결과를 제공하는 기능을 가지고 있습니다.

 

import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart'; // file_picker 패키지 임포트
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '아이케어',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TitleScreen(),
    );
  }
}

class TitleScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white, // 배경색을 흰색으로 설정
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            SizedBox(height: 50), // 위쪽 여백 추가
            Text(
              '자폐 조기진단 서비스',
              style: TextStyle(
                fontSize: 24,
                color: Colors.black, // 흰색 배경에 맞게 텍스트 색상을 검정색으로 설정
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 10),
            Text(
              '아이케어',
              style: TextStyle(
                fontSize: 40,
                color: Colors.black, // 흰색 배경에 맞게 텍스트 색상을 검정색으로 설정
                fontWeight: FontWeight.bold,
              ),
            ),
            SizedBox(height: 20), // 텍스트와 이미지 사이의 간격 추가
            Image.asset(
              'assets/images/KakaoTalk_20240823_141604130_02.jpg', // 이미지 경로
              fit: BoxFit.cover,
              width: 200, // 이미지 너비 설정
              height: 300, // 이미지 높이 설정
            ),
            SizedBox(height: 20), // 이미지와 시작하기 버튼 사이의 간격 추가
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => ConsentScreen()), // 개인정보 수집 동의 화면으로 이동
                );
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.white,  // 기존의 primary를 backgroundColor로 대체
                foregroundColor: Colors.black,   // 기존의 onPrimary를 foregroundColor로 대체
                padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text('시작하기'),
                  SizedBox(width: 5),
                  Icon(Icons.play_arrow),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ConsentScreen extends StatefulWidget {
  @override
  _ConsentScreenState createState() => _ConsentScreenState();
}

class _ConsentScreenState extends State<ConsentScreen> {
  bool isAgreed = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('개인정보 수집 동의')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Expanded(
              child: SingleChildScrollView(
                child: RichText(
                  text: const TextSpan(
                    children: [
                      TextSpan(
                        text: '1. 개인정보의 수집 및 이용 목적\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text:
                        '귀하의 자녀에 대한 얼굴 인식 데이터를 수집하여 자폐 조기진단 알고리즘을 개발하고, 이를 통해 자폐 진단의 정확성을 향상시키고자 합니다. 또한, 수집된 정보를 바탕으로 의사에게 해당 정보를 전달하여 자폐 조기진단에 필요한 자료로 활용합니다.\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.normal,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text: '2. 수집하는 개인정보 항목\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text:
                        '얼굴 이미지 데이터, 나이, 성별 등 기본 정보와 기타 자폐 진단에 필요한 추가 정보를 수집합니다.\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.normal,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text: '3. 개인정보의 보유 및 이용 기간\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text:
                        '수집된 개인정보는 연구 및 진단 목적이 완료될 때까지 보유하며, 목적 달성 후 즉시 파기됩니다. 단, 관련 법령에 따라 보관이 필요한 경우 해당 기간 동안 안전하게 보관됩니다.\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.normal,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text: '4. 개인정보의 제3자 제공\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text:
                        '수집된 개인정보는 의사 및 자폐 진단 전문가에게 제공될 수 있으며, 제공되는 정보는 자폐 조기진단 목적에만 사용됩니다. 이 외의 목적으로는 제3자에게 제공되지 않습니다.\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.normal,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text: '5. 개인정보의 안전성 확보 조치\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text:
                        '당사는 수집된 개인정보의 유출, 도난, 변조 등을 방지하기 위해 필요한 모든 기술적, 관리적 조치를 취하고 있습니다.\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.normal,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text: '6. 동의 거부 권리 및 동의 거부에 따른 불이익\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                      TextSpan(
                        text:
                        '귀하는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다. 다만, 동의를 거부할 경우 자폐 조기진단 서비스 제공이 제한될 수 있습니다.\n\n',
                        style: TextStyle(
                          fontWeight: FontWeight.normal,
                          fontSize: 16,
                          color: Colors.black,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            SizedBox(height: 20), // 간격 추가
            Align(
              alignment: Alignment.centerRight, // 버튼을 오른쪽에 배치
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  CheckboxListTile(
                    title: Text('동의'),
                    value: isAgreed,
                    onChanged: (bool? value) {
                      setState(() {
                        isAgreed = value ?? false;
                      });
                    },
                    controlAffinity: ListTileControlAffinity.leading, // 체크박스를 텍스트 왼쪽에 위치시킴
                  ),
                  SizedBox(height: 20), // 동의와 다음 버튼 사이의 간격 추가
                  ElevatedButton(
                    onPressed: isAgreed
                        ? () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => PersonalInfoScreen()), // 개인정보 입력 화면으로 이동
                      );
                    }
                        : null,
                    child: Text('다음'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class PersonalInfoScreen extends StatefulWidget {
  @override
  _PersonalInfoScreenState createState() => _PersonalInfoScreenState();
}

class _PersonalInfoScreenState extends State<PersonalInfoScreen> {
  final _formKey = GlobalKey<FormState>();

  String name = '';
  String age = '';
  String gender = '';
  String country = '';
  String medicalHistory = '';
  String familyHistory = '';
  String guardianContact = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('개인정보 입력')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: ListView(
            children: [
              _buildTextField('이름', (value) => name = value),
              _buildTextField('나이', (value) => age = value, keyboardType: TextInputType.number),
              _buildTextField('성별', (value) => gender = value),
              _buildTextField('나라', (value) => country = value),
              _buildTextField('과거 병력', (value) => medicalHistory = value),
              _buildTextField('가족력', (value) => familyHistory = value),
              _buildTextField('보호자 연락처', (value) => guardianContact = value, keyboardType: TextInputType.phone),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => SurveyScreen()), // 설문조사 화면으로 이동
                    );
                  }
                },
                child: Text('다음'),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildTextField(String labelText, Function(String) onChanged, {TextInputType keyboardType = TextInputType.text}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: TextFormField(
        decoration: InputDecoration(
          labelText: labelText,
          border: OutlineInputBorder(),
        ),
        onChanged: onChanged,
        keyboardType: keyboardType,
        validator: (value) {
          if (value == null || value.isEmpty) {
            return '$labelText을(를) 입력해 주세요';
          }
          return null;
        },
      ),
    );
  }
}

class SurveyScreen extends StatefulWidget {
  @override
  _SurveyScreenState createState() => _SurveyScreenState();
}

class _SurveyScreenState extends State<SurveyScreen> {
  final _answers = List.filled(10, -1);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('설문조사')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Expanded(
              child: ListView(
                children: [
                  _buildQuestion(1, '다른 사람들이 듣지 못하는 작은 소리를 자주 알아차린다.'),
                  _buildQuestion(2, '세부 사항보다는 전체적인 그림에 더 집중한다.'),
                  _buildQuestion(3, '동시에 여러 가지 일을 쉽게 할 수 있다.'),
                  _buildQuestion(4, '방해가 있으면 빠르게 원래 하던 일로 돌아갈 수 있다.'),
                  _buildQuestion(5, '대화 중에 숨겨진 의미를 쉽게 파악한다.'),
                  _buildQuestion(6, '듣는 사람이 지루해하는지를 안다.'),
                  _buildQuestion(7, '이야기를 읽을 때 등장인물의 의도를 파악하기 어렵다.'),
                  _buildQuestion(8, '사물의 종류에 대한 정보를 수집하는 것을 좋아한다.'),
                  _buildQuestion(9, '얼굴만 보고도 상대방이 무엇을 생각하거나 느끼는지 쉽게 파악한다.'),
                  _buildQuestion(10, '사람들의 의도를 파악하기 어렵다.'),
                ],
              ),
            ),
            ElevatedButton(
              onPressed: () {
                if (_answers.contains(-1)) {
                  _showAlertDialog(context);
                } else {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => UploadScreen()), // 업로드 화면으로 이동
                  );
                }
              },
              child: Text('다음'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildQuestion(int index, String question) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('$index. $question', style: TextStyle(fontSize: 16)),
        Row(
          children: [
            Expanded(child: Center(child: Text('전혀 아니다'))),
            Expanded(child: Center(child: Text('거의 아니다'))),
            Expanded(child: Center(child: Text('보통이다'))),
            Expanded(child: Center(child: Text('약간 그렇다'))),
            Expanded(child: Center(child: Text('매우 그렇다'))),
          ],
        ),
        Row(
          children: List.generate(5, (i) {
            return Expanded(
              child: Radio<int>(
                value: i + 1,
                groupValue: _answers[index - 1],
                onChanged: (int? value) {
                  setState(() {
                    _answers[index - 1] = value!;
                  });
                },
              ),
            );
          }),
        ),
        SizedBox(height: 16), // 질문 사이에 간격 추가
      ],
    );
  }

  void _showAlertDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('경고'),
          content: Text('모든 항목을 체크해 주세요.'),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(); // 경고창 닫기
              },
              child: Text('확인'),
            ),
          ],
        );
      },
    );
  }
}

class UploadScreen extends StatefulWidget {
  @override
  _UploadScreenState createState() => _UploadScreenState();
}

class _UploadScreenState extends State<UploadScreen> {
  String? _fileName;
  String? _result;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('이미지 또는 영상 업로드')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Expanded(
              child: _fileName == null
                  ? Center(child: Text('미디어 파일을 선택하세요'))
                  : Column(
                children: [
                  Text('선택된 파일: $_fileName'),
                  ElevatedButton(
                    onPressed: _uploadFile,
                    child: Text('업로드'),
                  ),
                ],
              ),
            ),
            if (_result != null)
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Text('결과: $_result', style: TextStyle(fontSize: 20)),
              ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _pickFile,
        child: Icon(Icons.add),
      ),
    );
  }

  Future<void> _pickFile() async {
    FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.any);

    if (result != null) {
      setState(() {
        _fileName = result.files.single.name;
      });
    }
  }

  Future<void> _uploadFile() async {
    // 파일 업로드 기능 주석 처리됨
    // var request = http.MultipartRequest('POST', Uri.parse('https://example.com/predict'));
    // request.files.add(await http.MultipartFile.fromPath('file', _fileName!));
    //
    // var response = await request.send();
    // var responseData = await http.Response.fromStream(response);
    // var result = json.decode(responseData.body);
    //
    // setState(() {
    //   _result = result['result'];
    // });
  }
}

1. main 함수와 MyApp 클래스

  • main 함수: 앱이 실행될 때 가장 먼저 호출되는 함수입니다. runApp(MyApp())을 호출하여 앱을 시작합니다.
  • MyApp 클래스: Flutter 애플리케이션의 기본 구조를 정의합니다. MaterialApp 위젯을 사용하여 앱의 테마와 초기 화면(TitleScreen)을 설정합니다.

2. TitleScreen 클래스

  • TitleScreen은 앱의 첫 번째 화면으로, 사용자가 앱에 진입할 때 보게 되는 환영 화면입니다. 여기서는 앱의 제목과 소개 텍스트를 보여주고, 이미지를 표시한 후 "시작하기" 버튼을 제공합니다.
  • 버튼 동작: 사용자가 "시작하기" 버튼을 누르면 ConsentScreen(개인정보 수집 동의 화면)으로 이동합니다.

3. ConsentScreen 클래스

  • 이 화면에서는 사용자가 개인정보 수집 및 이용에 대해 동의하는지 확인합니다. 앱은 동의 내용에 대해 자세히 설명하며, 사용자가 동의하면 PersonalInfoScreen으로 이동할 수 있는 "다음" 버튼이 활성화됩니다.

4. PersonalInfoScreen 클래스

  • 사용자는 이 화면에서 자신의 이름, 나이, 성별, 나라, 과거 병력, 가족력, 보호자 연락처 등의 개인정보를 입력합니다.
  • 폼 검증: 사용자가 입력한 데이터가 유효한지 확인하며, 모든 필드를 올바르게 채운 경우에만 다음 화면(SurveyScreen)으로 이동합니다.

5. SurveyScreen 클래스

  • 이 화면은 자폐 조기진단을 위한 설문조사입니다. 총 10개의 질문이 제공되며, 사용자는 각 질문에 대해 "전혀 아니다"에서 "매우 그렇다"까지의 5가지 옵션 중 하나를 선택할 수 있습니다.
  • 응답 검증: 사용자가 모든 질문에 답하지 않으면 경고 대화 상자가 표시됩니다. 모든 질문에 답한 경우, 다음 화면(UploadScreen)으로 이동합니다.

6. UploadScreen 클래스

  • 사용자는 이 화면에서 자폐 조기진단을 위해 얼굴 이미지나 영상을 업로드할 수 있습니다.
  • 파일 선택: FilePicker를 사용하여 사용자가 파일을 선택할 수 있도록 합니다.
  • 파일 업로드: 사용자가 선택한 파일을 서버에 업로드하는 기능을 제공합니다. (실제 업로드 기능은 주석 처리되어 있습니다.)