프로젝트 최초 커밋

This commit is contained in:
JiWoong Sul
2025-07-02 17:45:44 +09:00
commit e346f83c97
235 changed files with 23139 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/common/widgets/address_input.dart';
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
import 'package:superport/utils/validators.dart';
import 'package:superport/utils/phone_utils.dart';
class BranchCard extends StatefulWidget {
final GlobalKey cardKey;
final int index;
final Branch branch;
final TextEditingController nameController;
final TextEditingController contactNameController;
final TextEditingController contactPositionController;
final TextEditingController contactPhoneController;
final TextEditingController contactEmailController;
final FocusNode focusNode;
final List<String> positions;
final List<String> phonePrefixes;
final String selectedPhonePrefix;
final ValueChanged<String> onNameChanged;
final ValueChanged<Address> onAddressChanged;
final ValueChanged<String> onContactNameChanged;
final ValueChanged<String> onContactPositionChanged;
final ValueChanged<String> onContactPhoneChanged;
final ValueChanged<String> onContactEmailChanged;
final ValueChanged<String> onPhonePrefixChanged;
final VoidCallback onDelete;
const BranchCard({
Key? key,
required this.cardKey,
required this.index,
required this.branch,
required this.nameController,
required this.contactNameController,
required this.contactPositionController,
required this.contactPhoneController,
required this.contactEmailController,
required this.focusNode,
required this.positions,
required this.phonePrefixes,
required this.selectedPhonePrefix,
required this.onNameChanged,
required this.onAddressChanged,
required this.onContactNameChanged,
required this.onContactPositionChanged,
required this.onContactPhoneChanged,
required this.onContactEmailChanged,
required this.onPhonePrefixChanged,
required this.onDelete,
}) : super(key: key);
@override
_BranchCardState createState() => _BranchCardState();
}
class _BranchCardState extends State<BranchCard> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// 화면의 빈 공간 터치 시 포커스 해제
FocusScope.of(context).unfocus();
},
child: Card(
key: widget.cardKey,
margin: const EdgeInsets.only(bottom: 16.0),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'지점 #${widget.index + 1}',
style: AppThemeTailwind.subheadingStyle,
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: widget.onDelete,
),
],
),
const SizedBox(height: 8),
FormFieldWrapper(
label: '지점명',
isRequired: true,
child: TextFormField(
controller: widget.nameController,
focusNode: widget.focusNode,
decoration: const InputDecoration(hintText: '지점명을 입력하세요'),
onChanged: widget.onNameChanged,
validator: FormValidator.required('지점명은 필수입니다'),
),
),
AddressInput(
initialZipCode: widget.branch.address.zipCode,
initialRegion: widget.branch.address.region,
initialDetailAddress: widget.branch.address.detailAddress,
onAddressChanged: (zipCode, region, detailAddress) {
final address = Address(
zipCode: zipCode,
region: region,
detailAddress: detailAddress,
);
widget.onAddressChanged(address);
},
),
// 담당자 정보 - ContactInfoWidget 사용
ContactInfoWidget(
title: '담당자 정보',
contactNameController: widget.contactNameController,
contactPositionController: widget.contactPositionController,
contactPhoneController: widget.contactPhoneController,
contactEmailController: widget.contactEmailController,
positions: widget.positions,
selectedPhonePrefix: widget.selectedPhonePrefix,
phonePrefixes: widget.phonePrefixes,
onPhonePrefixChanged: widget.onPhonePrefixChanged,
onContactNameChanged: widget.onContactNameChanged,
onContactPositionChanged: widget.onContactPositionChanged,
onContactPhoneChanged: widget.onContactPhoneChanged,
onContactEmailChanged: widget.onContactEmailChanged,
compactMode: false, // compactMode를 false로 변경하여 한 줄로 표시
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import '../controllers/branch_form_controller.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/screens/company/widgets/contact_info_form.dart';
import 'package:superport/screens/common/widgets/address_input.dart';
import 'package:superport/screens/common/widgets/remark_input.dart';
/// 지점 입력 폼 위젯
///
/// BranchFormController를 받아서 입력 필드, 드롭다운, 포커스, 전화번호 등 UI/상태를 관리한다.
class BranchFormWidget extends StatelessWidget {
final BranchFormController controller;
final int index;
final void Function()? onRemove;
final void Function(Address)? onAddressChanged;
const BranchFormWidget({
Key? key,
required this.controller,
required this.index,
this.onRemove,
this.onAddressChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
key: controller.cardKey,
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: TextFormField(
controller: controller.nameController,
focusNode: controller.focusNode,
decoration: const InputDecoration(labelText: '지점명'),
onChanged: (value) => controller.updateField('name', value),
),
),
if (onRemove != null)
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: onRemove,
),
],
),
const SizedBox(height: 8),
// 주소 입력: 회사와 동일한 AddressInput 위젯 사용
AddressInput(
initialZipCode: controller.branch.address.zipCode,
initialRegion: controller.branch.address.region,
initialDetailAddress: controller.branch.address.detailAddress,
isRequired: false,
onAddressChanged: (zipCode, region, detailAddress) {
controller.updateAddress(
Address(
zipCode: zipCode,
region: region,
detailAddress: detailAddress,
),
);
if (onAddressChanged != null) {
onAddressChanged!(
Address(
zipCode: zipCode,
region: region,
detailAddress: detailAddress,
),
);
}
},
),
const SizedBox(height: 8),
// 담당자 정보 입력: ContactInfoForm 위젯으로 대체 (회사 담당자와 동일 UI)
ContactInfoForm(
contactNameController: controller.contactNameController,
contactPositionController: controller.contactPositionController,
contactPhoneController: controller.contactPhoneController,
contactEmailController: controller.contactEmailController,
positions: controller.positions,
selectedPhonePrefix: controller.selectedPhonePrefix,
phonePrefixes: controller.phonePrefixes,
onPhonePrefixChanged: (value) {
controller.updatePhonePrefix(value);
},
onNameSaved: (value) {
controller.updateField('contactName', value ?? '');
},
onPositionSaved: (value) {
controller.updateField('contactPosition', value ?? '');
},
onPhoneSaved: (value) {
controller.updateField('contactPhone', value ?? '');
},
onEmailSaved: (value) {
controller.updateField('contactEmail', value ?? '');
},
),
const SizedBox(height: 8),
// 비고 입력란
RemarkInput(controller: controller.remarkController),
],
),
),
);
}
}

View File

@@ -0,0 +1,374 @@
import 'package:flutter/material.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/screens/company/widgets/company_info_card.dart';
import 'package:pdf/widgets.dart' as pw; // PDF 생성용
import 'package:printing/printing.dart'; // PDF 프린트/미리보기용
import 'dart:typed_data'; // Uint8List
import 'package:pdf/pdf.dart'; // PdfColors, PageFormat 등 전체 임포트
import 'package:superport/screens/common/custom_widgets.dart'; // DataTableCard 사용을 위한 import
import 'package:flutter/services.dart'; // rootBundle 사용을 위한 import
/// 본사와 지점 리스트를 보여주는 다이얼로그 위젯
class CompanyBranchDialog extends StatelessWidget {
final Company mainCompany;
const CompanyBranchDialog({super.key, required this.mainCompany});
// 본사+지점 정보를 PDF로 생성하는 함수
Future<Uint8List> _buildPdf(final pw.Document pdf) async {
// 한글 폰트 로드 (lib/assets/fonts/NotoSansKR-VariableFont_wght.ttf)
final fontData = await rootBundle.load(
'lib/assets/fonts/NotoSansKR-VariableFont_wght.ttf',
);
final ttf = pw.Font.ttf(fontData);
final List<Branch> branchList = mainCompany.branches ?? [];
pdf.addPage(
pw.Page(
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'본사 및 지점 목록',
style: pw.TextStyle(
font: ttf, // 한글 폰트 적용
fontSize: 20,
fontWeight: pw.FontWeight.bold,
),
),
pw.SizedBox(height: 16),
pw.Table(
border: pw.TableBorder.all(color: PdfColors.grey800),
defaultVerticalAlignment: pw.TableCellVerticalAlignment.middle,
children: [
pw.TableRow(
decoration: pw.BoxDecoration(color: PdfColors.grey300),
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('구분', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('이름', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('우편번호', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('담당자', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('직책', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('전화번호', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('이메일', style: pw.TextStyle(font: ttf)),
),
],
),
// 본사
pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('본사', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
mainCompany.name,
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
mainCompany.address.zipCode,
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
mainCompany.contactName ?? '',
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
mainCompany.contactPosition ?? '',
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
mainCompany.contactPhone ?? '',
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
mainCompany.contactEmail ?? '',
style: pw.TextStyle(font: ttf),
),
),
],
),
// 지점
...branchList.map(
(branch) => pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('지점', style: pw.TextStyle(font: ttf)),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
branch.name,
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
branch.address.zipCode,
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
branch.contactName ?? '',
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
branch.contactPosition ?? '',
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
branch.contactPhone ?? '',
style: pw.TextStyle(font: ttf),
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text(
branch.contactEmail ?? '',
style: pw.TextStyle(font: ttf),
),
),
],
),
),
],
),
],
);
},
),
);
return pdf.save();
}
// 프린트 버튼 클릭 시 PDF 미리보기 및 인쇄
void _printPopupData() async {
final pdf = pw.Document();
await Printing.layoutPdf(
onLayout: (format) async {
return _buildPdf(pdf);
},
);
}
@override
Widget build(BuildContext context) {
final List<Branch> branchList = mainCompany.branches ?? [];
// 본사와 지점 정보를 한 리스트로 합침
final List<Map<String, dynamic>> displayList = [
{
'type': '본사',
'name': mainCompany.name,
'companyTypes': mainCompany.companyTypes,
'address': mainCompany.address,
'contactName': mainCompany.contactName,
'contactPosition': mainCompany.contactPosition,
'contactPhone': mainCompany.contactPhone,
'contactEmail': mainCompany.contactEmail,
},
...branchList.map(
(branch) => {
'type': '지점',
'name': branch.name,
'companyTypes': mainCompany.companyTypes,
'address': branch.address,
'contactName': branch.contactName,
'contactPosition': branch.contactPosition,
'contactPhone': branch.contactPhone,
'contactEmail': branch.contactEmail,
},
),
];
final double maxDialogHeight = MediaQuery.of(context).size.height * 0.7;
final double maxDialogWidth = MediaQuery.of(context).size.width * 0.8;
return Dialog(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxDialogHeight,
maxWidth: maxDialogWidth,
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'본사 및 지점 목록',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.print),
tooltip: '프린트',
onPressed: _printPopupData,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
],
),
const SizedBox(height: 16),
Expanded(
child: DataTableCard(
width: maxDialogWidth - 48,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(
width: maxDialogWidth - 48,
constraints: BoxConstraints(minWidth: 900),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: DataTable(
columns: const [
DataColumn(label: Text('번호')),
DataColumn(label: Text('구분')),
DataColumn(label: Text('회사명')),
DataColumn(label: Text('유형')),
DataColumn(label: Text('주소')),
DataColumn(label: Text('담당자')),
DataColumn(label: Text('직책')),
DataColumn(label: Text('전화번호')),
DataColumn(label: Text('이메일')),
],
rows:
displayList.asMap().entries.map((entry) {
final int index = entry.key;
final data = entry.value;
return DataRow(
cells: [
DataCell(Text('${index + 1}')),
DataCell(Text(data['type'])),
DataCell(Text(data['name'])),
DataCell(
Row(
children:
(data['companyTypes']
as List<CompanyType>)
.map(
(type) => Container(
margin:
const EdgeInsets.only(
right: 4,
),
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color:
type ==
CompanyType
.customer
? Colors
.blue
.shade50
: Colors
.green
.shade50,
borderRadius:
BorderRadius.circular(
8,
),
),
child: Text(
companyTypeToString(type),
style: TextStyle(
color:
type ==
CompanyType
.customer
? Colors
.blue
.shade800
: Colors
.green
.shade800,
fontWeight:
FontWeight.bold,
fontSize: 14,
),
),
),
)
.toList(),
),
),
DataCell(Text(data['address'].toString())),
DataCell(Text(data['contactName'] ?? '')),
DataCell(
Text(data['contactPosition'] ?? ''),
),
DataCell(Text(data['contactPhone'] ?? '')),
DataCell(Text(data['contactEmail'] ?? '')),
],
);
}).toList(),
),
),
),
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:superport/models/address_model.dart';
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/common/widgets/address_input.dart';
import 'package:superport/utils/validators.dart';
import 'package:superport/screens/company/widgets/company_name_autocomplete.dart';
import 'package:superport/screens/common/widgets/remark_input.dart';
class CompanyFormHeader extends StatelessWidget {
final TextEditingController nameController;
final FocusNode nameFocusNode;
final List<String> companyNames;
final List<String> filteredCompanyNames;
final bool showCompanyNameDropdown;
final Function(String) onCompanyNameSelected;
final Function() onShowMapPressed;
final ValueChanged<String?> onNameSaved;
final ValueChanged<Address> onAddressChanged;
final Address initialAddress;
final String nameLabel;
final String nameHint;
final TextEditingController remarkController;
const CompanyFormHeader({
Key? key,
required this.nameController,
required this.nameFocusNode,
required this.companyNames,
required this.filteredCompanyNames,
required this.showCompanyNameDropdown,
required this.onCompanyNameSelected,
required this.onShowMapPressed,
required this.onNameSaved,
required this.onAddressChanged,
this.initialAddress = const Address(),
required this.nameLabel,
required this.nameHint,
required this.remarkController,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 회사명/지점명
FormFieldWrapper(
label: nameLabel,
isRequired: true,
child: CompanyNameAutocomplete(
nameController: nameController,
nameFocusNode: nameFocusNode,
companyNames: companyNames,
filteredCompanyNames: filteredCompanyNames,
showCompanyNameDropdown: showCompanyNameDropdown,
onCompanyNameSelected: onCompanyNameSelected,
onNameSaved: onNameSaved,
label: nameLabel,
hint: nameHint,
),
),
// 주소 입력 위젯 (SRP에 따라 별도 컴포넌트로 분리)
AddressInput(
initialZipCode: initialAddress.zipCode,
initialRegion: initialAddress.region,
initialDetailAddress: initialAddress.detailAddress,
isRequired: false,
onAddressChanged: (zipCode, region, detailAddress) {
final address = Address(
zipCode: zipCode,
region: region,
detailAddress: detailAddress,
);
onAddressChanged(address);
},
),
const SizedBox(height: 12),
// 비고 입력란
RemarkInput(controller: remarkController),
],
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/models/address_model.dart';
/// 회사/지점 정보를 1행(1열)로 보여주는 재활용 위젯
class CompanyInfoCard extends StatelessWidget {
final String title; // 본사/지점 구분
final String name;
final List<CompanyType> companyTypes;
final Address address;
final String? contactName;
final String? contactPosition;
final String? contactPhone;
final String? contactEmail;
const CompanyInfoCard({
super.key,
required this.title,
required this.name,
required this.companyTypes,
required this.address,
this.contactName,
this.contactPosition,
this.contactPhone,
this.contactEmail,
});
@override
Widget build(BuildContext context) {
// 각 데이터가 없으면 빈 문자열로 표기
final String zipCode = address.zipCode.isNotEmpty ? address.zipCode : '';
final String displayName = name.isNotEmpty ? name : '';
final String displayContactName =
contactName != null && contactName!.isNotEmpty ? contactName! : '';
final String displayContactPosition =
contactPosition != null && contactPosition!.isNotEmpty
? contactPosition!
: '';
final String displayContactPhone =
contactPhone != null && contactPhone!.isNotEmpty ? contactPhone! : '';
final String displayContactEmail =
contactEmail != null && contactEmail!.isNotEmpty ? contactEmail! : '';
return Card(
color: Colors.grey.shade50,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 본사/지점 구분만 상단에 표기 (텍스트 크기 14로 축소)
Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
const SizedBox(height: 2), // 간격도 절반으로 축소
// 1행(1열)로 데이터만 표기
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Text(displayName, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 12),
Text(zipCode, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 12),
Text(
displayContactName,
style: const TextStyle(fontSize: 13),
),
const SizedBox(width: 12),
Text(
displayContactPosition,
style: const TextStyle(fontSize: 13),
),
const SizedBox(width: 12),
Text(
displayContactPhone,
style: const TextStyle(fontSize: 13),
),
const SizedBox(width: 12),
Text(
displayContactEmail,
style: const TextStyle(fontSize: 13),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:superport/utils/validators.dart';
class CompanyNameAutocomplete extends StatelessWidget {
final TextEditingController nameController;
final FocusNode nameFocusNode;
final List<String> companyNames;
final List<String> filteredCompanyNames;
final bool showCompanyNameDropdown;
final Function(String) onCompanyNameSelected;
final ValueChanged<String?> onNameSaved;
final String label;
final String hint;
const CompanyNameAutocomplete({
Key? key,
required this.nameController,
required this.nameFocusNode,
required this.companyNames,
required this.filteredCompanyNames,
required this.showCompanyNameDropdown,
required this.onCompanyNameSelected,
required this.onNameSaved,
required this.label,
required this.hint,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: nameController,
focusNode: nameFocusNode,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
labelText: label,
hintText: hint,
suffixIcon:
nameController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
nameController.clear();
},
)
: IconButton(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {},
),
),
validator: (value) => validateRequired(value, label),
onFieldSubmitted: (_) {
if (filteredCompanyNames.length == 1 && showCompanyNameDropdown) {
onCompanyNameSelected(filteredCompanyNames[0]);
}
},
onTap: () {},
onSaved: onNameSaved,
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height:
showCompanyNameDropdown
? (filteredCompanyNames.length > 4
? 200
: filteredCompanyNames.length * 50.0)
: 0,
margin: EdgeInsets.only(top: showCompanyNameDropdown ? 4 : 0),
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: GestureDetector(
onTap: () {},
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(77),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child:
filteredCompanyNames.isEmpty
? const Padding(
padding: EdgeInsets.all(12.0),
child: Text('검색 결과가 없습니다'),
)
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredCompanyNames.length,
separatorBuilder:
(context, index) => Divider(
height: 1,
color: Colors.grey.shade200,
),
itemBuilder: (context, index) {
final companyName = filteredCompanyNames[index];
final text = nameController.text.toLowerCase();
if (text.isEmpty) {
return ListTile(
dense: true,
title: Text(companyName),
onTap: () => onCompanyNameSelected(companyName),
);
}
// 일치하는 부분 찾기
final matchIndex = companyName
.toLowerCase()
.indexOf(text.toLowerCase());
if (matchIndex < 0) {
return ListTile(
dense: true,
title: Text(companyName),
onTap: () => onCompanyNameSelected(companyName),
);
}
return ListTile(
dense: true,
title: RichText(
text: TextSpan(
children: [
// 일치 이전 부분
if (matchIndex > 0)
TextSpan(
text: companyName.substring(
0,
matchIndex,
),
style: const TextStyle(
color: Colors.black,
),
),
// 일치하는 부분
TextSpan(
text: companyName.substring(
matchIndex,
matchIndex + text.length,
),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
// 일치 이후 부분
if (matchIndex + text.length <
companyName.length)
TextSpan(
text: companyName.substring(
matchIndex + text.length,
),
style: TextStyle(
color:
matchIndex == 0
? Colors.grey[600]
: Colors.black,
),
),
],
),
),
onTap: () => onCompanyNameSelected(companyName),
);
},
),
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/company/widgets/contact_info_widget.dart';
/// 담당자 정보 폼
///
/// 회사 등록 및 수정 화면에서 사용되는 담당자 정보 입력 폼
/// 내부적으로 공통 ContactInfoWidget을 사용하여 코드 재사용성 확보
class ContactInfoForm extends StatelessWidget {
final TextEditingController contactNameController;
final TextEditingController contactPositionController;
final TextEditingController contactPhoneController;
final TextEditingController contactEmailController;
final List<String> positions;
final String selectedPhonePrefix;
final List<String> phonePrefixes;
final ValueChanged<String> onPhonePrefixChanged;
final ValueChanged<String?> onNameSaved;
final ValueChanged<String?> onPositionSaved;
final ValueChanged<String?> onPhoneSaved;
final ValueChanged<String?> onEmailSaved;
const ContactInfoForm({
Key? key,
required this.contactNameController,
required this.contactPositionController,
required this.contactPhoneController,
required this.contactEmailController,
required this.positions,
required this.selectedPhonePrefix,
required this.phonePrefixes,
required this.onPhonePrefixChanged,
required this.onNameSaved,
required this.onPositionSaved,
required this.onPhoneSaved,
required this.onEmailSaved,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// ContactInfoWidget을 사용하여 담당자 정보 UI 구성
return ContactInfoWidget(
contactNameController: contactNameController,
contactPositionController: contactPositionController,
contactPhoneController: contactPhoneController,
contactEmailController: contactEmailController,
positions: positions,
selectedPhonePrefix: selectedPhonePrefix,
phonePrefixes: phonePrefixes,
onPhonePrefixChanged: onPhonePrefixChanged,
// 각 콜백 함수를 ContactInfoWidget의 onChanged 콜백과 연결
onContactNameChanged: (value) => onNameSaved?.call(value),
onContactPositionChanged: (value) => onPositionSaved?.call(value),
onContactPhoneChanged: (value) => onPhoneSaved?.call(value),
onContactEmailChanged: (value) => onEmailSaved?.call(value),
);
}
}

View File

@@ -0,0 +1,702 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:developer' as developer;
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/utils/validators.dart';
import 'package:superport/utils/phone_utils.dart';
import 'dart:math' as math;
/// 담당자 정보 위젯
///
/// 회사 및 지점의 담당자 정보를 입력받는 공통 위젯
/// SRP(단일 책임 원칙)에 따라 담당자 정보 입력 로직을 분리
class ContactInfoWidget extends StatefulWidget {
/// 위젯 제목
final String title;
/// 담당자 이름 컨트롤러
final TextEditingController contactNameController;
/// 담당자 직책 컨트롤러
final TextEditingController contactPositionController;
/// 담당자 전화번호 컨트롤러
final TextEditingController contactPhoneController;
/// 담당자 이메일 컨트롤러
final TextEditingController contactEmailController;
/// 직책 목록
final List<String> positions;
/// 선택된 전화번호 접두사
final String selectedPhonePrefix;
/// 전화번호 접두사 목록
final List<String> phonePrefixes;
/// 직책 컴팩트 모드 (Row 또는 Column 레이아웃 결정)
final bool compactMode;
/// 전화번호 접두사 변경 콜백
final ValueChanged<String> onPhonePrefixChanged;
/// 담당자 이름 변경 콜백
final ValueChanged<String> onContactNameChanged;
/// 담당자 직책 변경 콜백
final ValueChanged<String> onContactPositionChanged;
/// 담당자 전화번호 변경 콜백
final ValueChanged<String> onContactPhoneChanged;
/// 담당자 이메일 변경 콜백
final ValueChanged<String> onContactEmailChanged;
const ContactInfoWidget({
Key? key,
this.title = '담당자 정보',
required this.contactNameController,
required this.contactPositionController,
required this.contactPhoneController,
required this.contactEmailController,
required this.positions,
required this.selectedPhonePrefix,
required this.phonePrefixes,
required this.onPhonePrefixChanged,
required this.onContactNameChanged,
required this.onContactPositionChanged,
required this.onContactPhoneChanged,
required this.onContactEmailChanged,
this.compactMode = false,
}) : super(key: key);
@override
State<ContactInfoWidget> createState() => _ContactInfoWidgetState();
}
class _ContactInfoWidgetState extends State<ContactInfoWidget> {
bool _showPositionDropdown = false;
bool _showPhonePrefixDropdown = false;
final LayerLink _positionLayerLink = LayerLink();
final LayerLink _phonePrefixLayerLink = LayerLink();
OverlayEntry? _positionOverlayEntry;
OverlayEntry? _phonePrefixOverlayEntry;
final FocusNode _positionFocusNode = FocusNode();
final FocusNode _phonePrefixFocusNode = FocusNode();
@override
void initState() {
super.initState();
developer.log('ContactInfoWidget 초기화 완료', name: 'ContactInfoWidget');
_positionFocusNode.addListener(() {
if (_positionFocusNode.hasFocus) {
developer.log('직책 필드 포커스 얻음', name: 'ContactInfoWidget');
} else {
developer.log('직책 필드 포커스 잃음', name: 'ContactInfoWidget');
}
});
_phonePrefixFocusNode.addListener(() {
if (_phonePrefixFocusNode.hasFocus) {
developer.log('전화번호 접두사 필드 포커스 얻음', name: 'ContactInfoWidget');
} else {
developer.log('전화번호 접두사 필드 포커스 잃음', name: 'ContactInfoWidget');
}
});
}
@override
void dispose() {
_removeAllOverlays();
_positionFocusNode.dispose();
_phonePrefixFocusNode.dispose();
super.dispose();
}
void _togglePositionDropdown() {
developer.log(
'직책 드롭다운 토글: $_showPositionDropdown -> ${!_showPositionDropdown}',
name: 'ContactInfoWidget',
);
setState(() {
if (_showPositionDropdown) {
_removePositionOverlay();
} else {
_showPositionDropdown = true;
_showPhonePrefixDropdown = false;
_removePhonePrefixOverlay();
_showPositionOverlay();
}
});
}
void _togglePhonePrefixDropdown() {
developer.log(
'전화번호 접두사 드롭다운 토글: $_showPhonePrefixDropdown -> ${!_showPhonePrefixDropdown}',
name: 'ContactInfoWidget',
);
setState(() {
if (_showPhonePrefixDropdown) {
_removePhonePrefixOverlay();
} else {
_showPhonePrefixDropdown = true;
_showPositionDropdown = false;
_removePositionOverlay();
_showPhonePrefixOverlay();
}
});
}
void _removePositionOverlay() {
_positionOverlayEntry?.remove();
_positionOverlayEntry = null;
_showPositionDropdown = false;
}
void _removePhonePrefixOverlay() {
_phonePrefixOverlayEntry?.remove();
_phonePrefixOverlayEntry = null;
_showPhonePrefixDropdown = false;
}
void _removeAllOverlays() {
_removePositionOverlay();
_removePhonePrefixOverlay();
}
void _closeAllDropdowns() {
if (_showPositionDropdown || _showPhonePrefixDropdown) {
developer.log('모든 드롭다운 닫기', name: 'ContactInfoWidget');
setState(() {
_removeAllOverlays();
});
}
}
void _showPositionOverlay() {
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
final availableHeight =
MediaQuery.of(context).size.height - offset.dy - 100;
final maxHeight = math.min(300.0, availableHeight);
_positionOverlayEntry = OverlayEntry(
builder:
(context) => Positioned(
width: 200,
child: CompositedTransformFollower(
link: _positionLayerLink,
showWhenUnlinked: false,
offset: const Offset(0, 45),
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(4),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
constraints: BoxConstraints(maxHeight: maxHeight),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...widget.positions.map(
(position) => InkWell(
onTap: () {
developer.log(
'직책 선택됨: $position',
name: 'ContactInfoWidget',
);
setState(() {
widget.contactPositionController.text =
position;
widget.onContactPositionChanged(position);
_removePositionOverlay();
});
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
width: double.infinity,
child: Text(position),
),
),
),
InkWell(
onTap: () {
developer.log(
'직책 기타(직접 입력) 선택됨',
name: 'ContactInfoWidget',
);
_removePositionOverlay();
widget.contactPositionController.clear();
widget.onContactPositionChanged('');
_positionFocusNode.requestFocus();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
width: double.infinity,
child: const Text('기타 (직접 입력)'),
),
),
],
),
),
),
),
),
),
);
Overlay.of(context).insert(_positionOverlayEntry!);
}
void _showPhonePrefixOverlay() {
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
final availableHeight =
MediaQuery.of(context).size.height - offset.dy - 100;
final maxHeight = math.min(300.0, availableHeight);
_phonePrefixOverlayEntry = OverlayEntry(
builder:
(context) => Positioned(
width: 200,
child: CompositedTransformFollower(
link: _phonePrefixLayerLink,
showWhenUnlinked: false,
offset: const Offset(0, 45),
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(4),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
constraints: BoxConstraints(maxHeight: maxHeight),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...widget.phonePrefixes.map(
(prefix) => InkWell(
onTap: () {
developer.log(
'전화번호 접두사 선택됨: $prefix',
name: 'ContactInfoWidget',
);
widget.onPhonePrefixChanged(prefix);
setState(() {
_removePhonePrefixOverlay();
});
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
width: double.infinity,
child: Text(prefix),
),
),
),
InkWell(
onTap: () {
developer.log(
'전화번호 접두사 직접 입력 선택됨',
name: 'ContactInfoWidget',
);
_removePhonePrefixOverlay();
_phonePrefixFocusNode.requestFocus();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
width: double.infinity,
child: const Text('기타 (직접 입력)'),
),
),
],
),
),
),
),
),
),
);
Overlay.of(context).insert(_phonePrefixOverlayEntry!);
}
@override
Widget build(BuildContext context) {
developer.log(
'ContactInfoWidget 빌드 시작: 직책 드롭다운=$_showPositionDropdown, 전화번호 접두사 드롭다운=$_showPhonePrefixDropdown',
name: 'ContactInfoWidget',
);
// 컴팩트 모드에 따라 다른 레이아웃 생성
return FormFieldWrapper(
label: widget.title,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
widget.compactMode ? _buildCompactLayout() : _buildDefaultLayout(),
),
);
}
// 기본 레이아웃 (한 줄에 모든 필드 표시)
List<Widget> _buildDefaultLayout() {
return [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 담당자 이름
Expanded(
flex: 3,
child: TextFormField(
controller: widget.contactNameController,
decoration: const InputDecoration(
hintText: '이름',
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
),
onTap: () {
developer.log('이름 필드 터치됨', name: 'ContactInfoWidget');
_closeAllDropdowns();
},
onChanged: widget.onContactNameChanged,
),
),
const SizedBox(width: 8),
// 담당자 직책
Expanded(
flex: 2,
child: CompositedTransformTarget(
link: _positionLayerLink,
child: Stack(
children: [
TextFormField(
controller: widget.contactPositionController,
focusNode: _positionFocusNode,
decoration: InputDecoration(
hintText: '직책',
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_drop_down, size: 20),
padding: EdgeInsets.zero,
onPressed: () {
developer.log(
'직책 드롭다운 버튼 클릭됨',
name: 'ContactInfoWidget',
);
_togglePositionDropdown();
},
),
),
onTap: () {
// 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입
_closeAllDropdowns();
},
onChanged: widget.onContactPositionChanged,
),
],
),
),
),
const SizedBox(width: 8),
// 전화번호 접두사
Expanded(
flex: 2,
child: CompositedTransformTarget(
link: _phonePrefixLayerLink,
child: Stack(
children: [
TextFormField(
controller: TextEditingController(
text: widget.selectedPhonePrefix,
),
focusNode: _phonePrefixFocusNode,
decoration: InputDecoration(
hintText: '국가번호',
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_drop_down, size: 20),
padding: EdgeInsets.zero,
onPressed: () {
developer.log(
'전화번호 접두사 드롭다운 버튼 클릭됨',
name: 'ContactInfoWidget',
);
_togglePhonePrefixDropdown();
},
),
),
onTap: () {
// 필드를 터치했을 때는 드롭다운을 열지 않고 직접 입력 모드로 진입
_closeAllDropdowns();
},
onChanged: (value) {
if (value.isNotEmpty) {
widget.onPhonePrefixChanged(value);
}
},
),
],
),
),
),
const SizedBox(width: 8),
// 담당자 전화번호
Expanded(
flex: 3,
child: TextFormField(
controller: widget.contactPhoneController,
decoration: const InputDecoration(
hintText: '전화번호',
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
PhoneUtils.phoneInputFormatter,
],
onTap: () {
developer.log('전화번호 필드 터치됨', name: 'ContactInfoWidget');
_closeAllDropdowns();
},
validator: validatePhoneNumber,
onChanged: widget.onContactPhoneChanged,
),
),
const SizedBox(width: 8),
// 담당자 이메일
Expanded(
flex: 6,
child: TextFormField(
controller: widget.contactEmailController,
decoration: const InputDecoration(
hintText: '이메일',
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
),
keyboardType: TextInputType.emailAddress,
onTap: () {
developer.log('이메일 필드 터치됨', name: 'ContactInfoWidget');
_closeAllDropdowns();
},
validator: FormValidator.email(),
onChanged: widget.onContactEmailChanged,
),
),
],
),
];
}
// 컴팩트 레이아웃 (여러 줄에 필드 표시)
List<Widget> _buildCompactLayout() {
return [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 담당자 이름
Expanded(
child: TextFormField(
controller: widget.contactNameController,
decoration: const InputDecoration(
hintText: '담당자 이름',
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
),
onTap: () {
developer.log('이름 필드 터치됨', name: 'ContactInfoWidget');
_closeAllDropdowns();
},
onChanged: widget.onContactNameChanged,
),
),
const SizedBox(width: 16),
// 담당자 직책
Expanded(
child: CompositedTransformTarget(
link: _positionLayerLink,
child: InkWell(
onTap: _togglePositionDropdown,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 15,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
widget.contactPositionController.text.isEmpty
? '직책 선택'
: widget.contactPositionController.text,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color:
widget.contactPositionController.text.isEmpty
? Colors.grey.shade600
: Colors.black,
),
),
),
const Icon(Icons.arrow_drop_down),
],
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 전화번호 (접두사 + 번호)
Expanded(
child: Row(
children: [
// 전화번호 접두사
CompositedTransformTarget(
link: _phonePrefixLayerLink,
child: InkWell(
onTap: _togglePhonePrefixDropdown,
child: Container(
width: 70,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 14,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(4),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.selectedPhonePrefix,
style: const TextStyle(fontSize: 14),
),
const Icon(Icons.arrow_drop_down, size: 18),
],
),
),
),
),
// 전화번호
Expanded(
child: TextFormField(
controller: widget.contactPhoneController,
decoration: const InputDecoration(
hintText: '전화번호',
border: OutlineInputBorder(
borderRadius: BorderRadius.horizontal(
left: Radius.zero,
right: Radius.circular(4),
),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
PhoneUtils.phoneInputFormatter,
],
keyboardType: TextInputType.phone,
onTap: _closeAllDropdowns,
onChanged: widget.onContactPhoneChanged,
validator: validatePhoneNumber,
),
),
],
),
),
const SizedBox(width: 16),
// 이메일
Expanded(
child: TextFormField(
controller: widget.contactEmailController,
decoration: const InputDecoration(
hintText: '이메일 주소',
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 15,
),
),
keyboardType: TextInputType.emailAddress,
onTap: _closeAllDropdowns,
onChanged: widget.onContactEmailChanged,
validator: FormValidator.email(),
),
),
],
),
];
}
}

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:superport/models/company_model.dart';
/// 중복된 회사명을 확인하는 대화상자
class DuplicateCompanyDialog extends StatelessWidget {
final Company company;
const DuplicateCompanyDialog({Key? key, required this.company})
: super(key: key);
static void show(BuildContext context, Company company) {
showDialog(
context: context,
builder: (context) => DuplicateCompanyDialog(company: company),
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('중복된 회사'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('동일한 이름의 회사가 이미 등록되어 있습니다.'),
const SizedBox(height: 16),
Text('회사명: ${company.name}'),
Text('주소: ${company.address ?? ''}'),
Text('담당자: ${company.contactName ?? ''}'),
Text('직책: ${company.contactPosition ?? ''}'),
Text('연락처: ${company.contactPhone ?? ''}'),
Text('이메일: ${company.contactEmail ?? ''}'),
const SizedBox(height: 8),
if (company.branches != null && company.branches!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'지점 정보:',
style: TextStyle(fontWeight: FontWeight.bold),
),
...company.branches!.map(
(branch) => Padding(
padding: const EdgeInsets.only(left: 8.0, top: 4.0),
child: Text(
'${branch.name}: ${branch.address ?? ''} (담당자: ${branch.contactName ?? ''}, 직책: ${branch.contactPosition ?? ''}, 연락처: ${branch.contactPhone ?? ''})',
),
),
),
],
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('확인'),
),
],
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
/// 주소에 대한 지도 대화상자를 표시합니다.
class MapDialog extends StatelessWidget {
final String address;
const MapDialog({Key? key, required this.address}) : super(key: key);
static void show(BuildContext context, String address) {
if (address.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('주소를 먼저 입력해주세요.'),
duration: Duration(seconds: 2),
),
);
return;
}
showDialog(
context: context,
builder: (BuildContext context) {
return MapDialog(address: address);
},
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.7,
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'주소 지도 보기',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const SizedBox(height: 8),
Text('주소: $address', style: const TextStyle(fontSize: 14)),
const SizedBox(height: 16),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.map,
size: 64,
color: AppThemeTailwind.primary,
),
const SizedBox(height: 16),
Text(
'여기에 주소 "$address"에 대한\n지도가 표시됩니다.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade700),
),
const SizedBox(height: 24),
Text(
'실제 구현 시에는 Google Maps 또는\n다른 지도 서비스 API를 연동하세요.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontStyle: FontStyle.italic,
),
),
],
),
),
),
),
],
),
),
);
}
}