뜬금없이 앱 개발을 한번 해보고 싶다는 마음이 생겨서 한번 해봤슴다
기존에 window 환경에서 부동산 정보를 엑셀로 저장하는 파이썬 코드를 짜놓은게 있었는데
이걸 활용해서 만들어 보겠슴다
친구한테 만들어준거라서 친구가 관심있는 특정 아파트의 id들로 하드코딩 되어있슴다.
윈도우 환경에서 안드로이드 앱 개발했고, IOS는 맥북 없으면 안된다고 해서 포기.
우선 환경 설정은 아래 유투브 참고함.
기반이 되는 파이썬 코드
import csv
import requests
import os
from datetime import datetime
# 현재 날짜와 시간을 이용해 파일명에 추가
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
file_name = f"D:\\Real Estate\\real_estate_data_{current_time}.csv"
cookies = {
'NNB': 'ZBK7UVCVVPZWC',
'NAC': 'UosYBQgt7vGZ',
'NACT': '1',
'SRT30': '1737028065',
'SRT5': '1737028065',
'REALESTATE': 'Thu%20Jan%2016%202025%2020%3A48%3A11%20GMT%2B0900%20(Korean%20Standard%20Time)',
'_fwb': '25u5neHwcKQK4wFt5TB5LH.1737028092346',
'BUC': 'WR7rgEVpHVSHP5oYnVmTK1y2VWrqD4IH4fE4pTKO1PM=',
}
headers = {
'accept': '*/*',
'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlJFQUxFU1RBVEUiLCJpYXQiOjE3MzcwMjgwOTEsImV4cCI6MTczNzAzODg5MX0.J-Qe7JnfL9I8uofNisWclKfSW0HqttF5iZaw_lUsoT8',
'priority': 'u=1, i',
'referer': 'https://new.land.naver.com/complexes/145684?ms=37.485702,126.7227337,17&a=APT:ABYG:JGC:PRE&e=RETAIL',
'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
}
data_rows = []
complex_ids = [
("145684", "해링턴 플레이스"),
("131187", "부평 한라비발디트레비앙"),
("151242", "e편한세상부평역센트럴파크"),
("133631", "e편한세상시티부평역"),
("121553", "부평역화성파크드림"),
("103413", "부평센트럴포레스트")
]
for complex_id, complex_name in complex_ids:
for trade_type in ["A1", "B1"]: # 매매(A1)와 전세(B1)를 처리
page = 1
while True:
url = f'https://new.land.naver.com/api/articles/complex/{complex_id}?realEstateType=APT%3AABYG%3AJGC%3APRE&tradeType={trade_type}&tag=%3A%3A%3A%3A%3A%3A%3A%3A&rentPriceMin=0&rentPriceMax=900000000&priceMin=0&priceMax=900000000&areaMin=0&areaMax=900000000&oldBuildYears&recentlyBuildYears&minHouseHoldCount&maxHouseHoldCount&showArticle=false&sameAddressGroup=false&minMaintenanceCost&maxMaintenanceCost&priceType=RETAIL&directions=&page={page}&complexNo={complex_id}&buildingNos=&areaNos=&type=list&order=rank'
response = requests.get(url, cookies=cookies, headers=headers)
if response.status_code != 200:
print(f"Failed to retrieve data for {complex_name} (TradeType: {trade_type}), page {page}. Status code: {response.status_code}")
break
data = response.json()
articles = data.get('articleList', [])
# If no articles are returned, we've reached the last page
if not articles:
print(f"No more data to fetch for {complex_name} (TradeType: {trade_type}).")
break
# Collect the data rows
for article in articles:
data_rows.append([
article.get("articleNo"),
article.get("articleName"),
article.get("realEstateTypeName"),
article.get("tradeTypeName"),
article.get("dealOrWarrantPrc"),
article.get("areaName"),
article.get("area1"),
article.get("area2"),
article.get("direction"),
article.get("articleConfirmYmd"),
article.get("buildingName"),
", ".join(article.get("tagList", [])),
article.get("latitude"),
article.get("longitude"),
article.get("realtorName"),
complex_name
])
if trade_type=="A1":
print(f"Page {page} for {complex_name} (TradeType: {trade_type}) 매매 processed.")
elif trade_type=="B1":
print(f"Page {page} for {complex_name} (TradeType: {trade_type}) 전세 processed.")
page += 1
# Save to CSV and sort by Price
with open(file_name, mode='w', newline='', encoding='utf-8-sig') as file:
writer = csv.writer(file)
# Write the header row
header = [
"Article Number", "Article Name", "Real Estate Type", "Trade Type", "Price",
"Area Name", "Area1", "Area2", "Direction", "Confirm Date",
"Building Name", "Tags", "Latitude", "Longitude", "Realtor Name", "Complex Name"
]
writer.writerow(header)
# Sort rows by Price (convert price to integer for sorting)
def price_to_int(price):
try:
if isinstance(price, str):
return int(price.replace(",", "").replace("억", "0000").replace("만원", ""))
except ValueError:
pass
return 0
sorted_rows = sorted(data_rows, key=lambda x: price_to_int(x[4]))
# Write sorted rows
writer.writerows(sorted_rows)
print(f"Data successfully saved and sorted to {file_name}")
D:\FlutterSource\room_search 경로에 room_search라는 프로젝트명으로 플러터 프로젝트 생성함.
D:\FlutterSource\room_search\pubspec.yaml
name: room_search
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.7.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
http: ^0.13.6 # 또는 최신 버전
dio: ^5.1.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
D:\FlutterSource\room_search\android\app\src\main\AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Add permission for Internet access -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="room_search"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
D:\FlutterSource\room_search\lib\main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Real Estate Data',
theme: ThemeData(primarySwatch: Colors.blue),
home: RealEstatePage(),
);
}
}
class RealEstatePage extends StatefulWidget {
@override
_RealEstatePageState createState() => _RealEstatePageState();
}
class _RealEstatePageState extends State<RealEstatePage> {
late Future<List<Map<String, dynamic>>> _realEstateData;
String _searchQuery = ''; // 검색할 아파트 이름을 저장할 변수
@override
void initState() {
super.initState();
_realEstateData = fetchRealEstateData();
}
Future<List<Map<String, dynamic>>> fetchRealEstateData() async {
final complexList = [
{'id': '145684', 'name': '해링턴 플레이스'},
{'id': '131187', 'name': '부평 한라비발디트레비앙'},
{'id': '151242', 'name': 'e편한세상부평역센트럴파크'},
{'id': '133631', 'name': 'e편한세상시티부평역'},
{'id': '121553', 'name': '부평역화성파크드림'},
{'id': '103413', 'name': '부평센트럴포레스트'},
];
final List<Map<String, dynamic>> allArticles = [];
final cookieHeader =
'NNB=ZBK7UVCVVPZWC; NAC=UosYBQgt7vGZ; NACT=1; SRT30=1737028065; SRT5=1737028065; REALESTATE=Thu%20Jan%2016%202025%2020%3A48%3A11%20GMT%2B0900%20(Korean%20Standard%20Time); _fwb=25u5neHwcKQK4wFt5TB5LH.1737028092346; BUC=WR7rgEVpHVSHP5oYnVmTK1y2VWrqD4IH4fE4pTKO1PM=';
final headers = {
'accept': '*/*',
'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'referer': 'https://new.land.naver.com/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'cookie': cookieHeader,
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlJFQUxFU1RBVEUiLCJpYXQiOjE3MzcwMjgwOTEsImV4cCI6MTczNzAzODg5MX0.J-Qe7JnfL9I8uofNisWclKfSW0HqttF5iZaw_lUsoT8',
};
try {
for (final complex in complexList) {
final id = complex['id'];
final name = complex['name'];
final url =
'https://new.land.naver.com/api/articles/complex/$id?realEstateType=APT%3AABYG%3AJGC%3APRE&tradeType=B1&page=1&complexNo=$id';
final response = await http.get(Uri.parse(url), headers: headers);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final articles = data['articleList'] ?? [];
allArticles.addAll(articles.map<Map<String, dynamic>>((article) {
return {
'articleNo': article['articleNo'],
'title': article['articleName'],
'realEstateType': article['realEstateTypeName'],
'tradeType': article['tradeTypeName'],
'price': article['dealOrWarrantPrc'],
'areaName': article['areaName'],
'area1': article['area1'],
'area2': article['area2'],
'direction': article['direction'],
'confirmDate': article['articleConfirmYmd'],
'buildingName': article['buildingName'],
'tags': article['tagList']?.join(', ') ?? '',
'latitude': article['latitude'],
'longitude': article['longitude'],
'realtorName': article['realtorName'],
'complex': name,
};
}));
} else {
print('❌ ${name} 응답 오류: ${response.statusCode}');
}
}
return allArticles;
} catch (e) {
print('❌ 예외 발생: $e');
throw Exception('예외 발생: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Real Estate Listings')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(
hintText: '아파트명 검색',
border: OutlineInputBorder(),
),
onChanged: (query) {
setState(() {
_searchQuery = query;
});
},
),
),
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>( // 데이터를 비동기적으로 받아옵니다.
future: _realEstateData, // fetchRealEstateData() 호출
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator()); // 로딩 중
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}')); // 에러 발생
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('데이터 없음')); // 데이터 없을 경우
} else {
// 아파트명으로 검색한 항목만 필터링
final filteredData = snapshot.data!.where((item) {
return item['complex']
.toString()
.toLowerCase()
.contains(_searchQuery.toLowerCase());
}).toList();
return ListView.builder(
itemCount: filteredData.length, // 필터링된 데이터의 길이
itemBuilder: (context, index) {
final item = filteredData[index]; // 각 항목
return ListTile(
title: Text(item['title']), // 아파트 이름
subtitle: Text(
'단지: ${item['complex']} | 가격: ${item['price']} | 면적: ${item['areaName']} | 방: ${item['area1']} | 방향: ${item['direction']}', // 상세 내용
),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('상세 정보', style: Theme.of(context).textTheme.headlineMedium),
SizedBox(height: 8),
Text('건물명: ${item['buildingName']}'),
Text('가격: ${item['price']}'),
Text('면적: ${item['areaName']}'),
Text('방: ${item['area1']}'),
Text('가격 확인일: ${item['confirmDate']}'),
Text('중개인: ${item['realtorName']}'),
Text('태그: ${item['tags']}'),
Text('위도: ${item['latitude']}'),
Text('경도: ${item['longitude']}'),
],
),
);
},
);
},
);
},
);
}
},
),
),
],
),
);
}
}
이렇게 한 후에 Android Studio 상에서 터미널 열어서 아래 명령어를 쳐서 앱 컴파일을 할 수가 있음.
flutter build apk --release
컴파일된 apk가 저장되는 공간은 하기와 같음.
D:\FlutterSource\room_search\build\app\outputs\flutter-apk\app-release.apk
디버깅 하는 방법
갤럭시 폰에서 설정 -> 휴대전화 정보 -> 소프트웨어 정보 -> 빌드번호 7번 이상 클릭하면
설정 -> 개발자 옵션 히든 메뉴가 생김.
개발자 옵션에서 디버깅 메뉴의 USB 디버깅 옵션을 켜두고 컴퓨터에 USB로 연결해서 파일 전송 모드로 변경해둔다.
연결 허용할지 말지 폰에서 뜰건데 허용 해주고~
안드로이드 스튜디오 터미널에서 아래 명령어 쳐서 내 디바이스가 잘 인식됬는지 확인 가능함.
flutter devices
결과물
개인용으로 사용하려고 만든거라서 상업용으로 사용하거나 하면 안됩니다~~