본문 바로가기
카테고리 없음

[Flutter] 부동산 시세 파악용 안드로이드 앱

by TYB 2025. 5. 3.
반응형

뜬금없이 앱 개발을 한번 해보고 싶다는 마음이 생겨서 한번 해봤슴다

 

기존에 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

 

 


결과물

 

 

개인용으로 사용하려고 만든거라서 상업용으로 사용하거나 하면 안됩니다~~

반응형